feat: add YouTube segments CLI using yt-dlp
This commit is contained in:
101
CONTEXT.md
Normal file
101
CONTEXT.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# YouTube Segments Downloader
|
||||||
|
|
||||||
|
A CLI tool that downloads the most watched segments (chapters) of a YouTube video using the battle-tested `yt-dlp` library.
|
||||||
|
|
||||||
|
## Project Purpose
|
||||||
|
|
||||||
|
This project provides a command-line interface for extracting and downloading video chapters/segments from YouTube videos. It's built with Bun, TypeScript, and integrates with yt-dlp for robust video downloading.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Chapter Extraction**: Automatically detects and extracts video chapters marked by creators
|
||||||
|
- **Segment Download**: Downloads each chapter as a separate video file
|
||||||
|
- **Full Video Download**: Also saves the complete video for reference
|
||||||
|
- **Chapter List Export**: Generates a text file with chapter timestamps and titles
|
||||||
|
- **Customizable Output**: Configurable output directory and video format
|
||||||
|
- **Bun Runtime**: Fast execution with Bun's native TypeScript support
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### CLI Entry Point
|
||||||
|
- [`src/cli/index.ts`](src/cli/index.ts) - Main CLI entry point with argument parsing
|
||||||
|
|
||||||
|
### Core Modules
|
||||||
|
- [`src/cli/args.ts`](src/cli/args.ts) - Command-line argument parser
|
||||||
|
- [`src/cli/downloader.ts`](src/cli/downloader.ts) - yt-dlp integration and segment download logic
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
- **Runtime**: Bun
|
||||||
|
- **Language**: TypeScript
|
||||||
|
- **Video Processing**: yt-dlp (external dependency)
|
||||||
|
- **Framework**: Next.js 15 (for potential web interface)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) must be installed on the system
|
||||||
|
- [FFmpeg](https://ffmpeg.org/) is recommended for better format handling
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Make the CLI accessible (optional, requires linking)
|
||||||
|
# bun link
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic usage
|
||||||
|
bun cli "https://www.youtube.com/watch?v=VIDEO_ID"
|
||||||
|
|
||||||
|
# With custom output directory
|
||||||
|
bun cli "https://www.youtube.com/watch?v=VIDEO_ID" -o ./my-videos
|
||||||
|
|
||||||
|
# With specific format
|
||||||
|
bun cli "https://www.youtube.com/watch?v=VIDEO_ID" -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best"
|
||||||
|
|
||||||
|
# Skip chapter extraction
|
||||||
|
bun cli "https://www.youtube.com/watch?v=VIDEO_ID" --no-chapters
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command-Line Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `<url>` | YouTube video URL (required) |
|
||||||
|
| `-o, --output <dir>` | Output directory (default: `./downloads`) |
|
||||||
|
| `-f, --format <fmt>` | Video format (default: `best`) |
|
||||||
|
| `--no-chapters` | Skip chapter extraction |
|
||||||
|
| `-h, --help` | Show help message |
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
|
||||||
|
After running the CLI, the output directory will contain:
|
||||||
|
|
||||||
|
```
|
||||||
|
downloads/
|
||||||
|
├── video_title_01_chapter_name.ext
|
||||||
|
├── video_title_02_another_chapter.ext
|
||||||
|
├── ...
|
||||||
|
├── video_title_full.ext
|
||||||
|
└── video_title_chapters.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Major Changes
|
||||||
|
|
||||||
|
### v0.1.0 (2026-01-14)
|
||||||
|
- Initial project setup
|
||||||
|
- Created CLI tool with chapter extraction
|
||||||
|
- Integrated yt-dlp for video downloading
|
||||||
|
- Added argument parsing and help documentation
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The CLI requires `yt-dlp` to be installed on the system (`pip install yt-dlp` or `brew install yt-dlp`)
|
||||||
|
- Chapters are extracted from video metadata - creators must have added them
|
||||||
|
- If no chapters are found, the full video is downloaded
|
||||||
|
- Chapter titles and timestamps are saved to a text file for reference
|
||||||
5
bun.lock
5
bun.lock
@@ -12,6 +12,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.3",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
"@types/bun": "^1.3.6",
|
||||||
"@types/node": "^24.10.2",
|
"@types/node": "^24.10.2",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -185,6 +186,8 @@
|
|||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
@@ -299,6 +302,8 @@
|
|||||||
|
|
||||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||||
|
|
||||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||||
|
|
||||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -1,12 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "nextjs-template",
|
"name": "yt-segments",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"yt-segments": "./src/cli/index.ts"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"cli": "bun run src/cli/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
@@ -14,14 +20,15 @@
|
|||||||
"next": "15.5.9"
|
"next": "15.5.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.9.3",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
"@types/bun": "^1.3.6",
|
||||||
"@types/node": "^24.10.2",
|
"@types/node": "^24.10.2",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
|
||||||
"tailwindcss": "^4.1.17",
|
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-config-next": "^15.5.7",
|
"eslint-config-next": "^15.5.7",
|
||||||
"@eslint/eslintrc": "^3.3.3"
|
"tailwindcss": "^4.1.17",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/cli/args.ts
Normal file
58
src/cli/args.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
export interface CliArgs {
|
||||||
|
url?: string;
|
||||||
|
output: string;
|
||||||
|
format: string;
|
||||||
|
noChapters: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseArgs(): CliArgs {
|
||||||
|
const args: CliArgs = {
|
||||||
|
output: "./downloads",
|
||||||
|
format: "best",
|
||||||
|
noChapters: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const rawArgs = Bun.argv;
|
||||||
|
|
||||||
|
for (let i = 0; i < rawArgs.length; i++) {
|
||||||
|
const arg = rawArgs[i];
|
||||||
|
const nextArg = rawArgs[i + 1];
|
||||||
|
|
||||||
|
if (arg === "-o" || arg === "--output") {
|
||||||
|
args.output = nextArg || "./downloads";
|
||||||
|
i++;
|
||||||
|
} else if (arg === "-f" || arg === "--format") {
|
||||||
|
args.format = nextArg || "best";
|
||||||
|
i++;
|
||||||
|
} else if (arg === "--no-chapters") {
|
||||||
|
args.noChapters = true;
|
||||||
|
} else if (arg === "-h" || arg === "--help") {
|
||||||
|
console.log(`YouTube Video Segments Downloader
|
||||||
|
|
||||||
|
Usage: yt-segments <url> [options]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<url> YouTube video URL (required)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-o, --output <dir> Output directory (default: ./downloads)
|
||||||
|
-f, --format <fmt> Video format (default: best)
|
||||||
|
--no-chapters Skip chapter extraction
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
yt-segments "https://www.youtube.com/watch?v=abc123"
|
||||||
|
yt-segments "https://youtu.be/abc123" -o ./videos -f mp4
|
||||||
|
yt-segments "https://www.youtube.com/watch?v=abc123" --no-chapters
|
||||||
|
`);
|
||||||
|
process.exit(0);
|
||||||
|
} else if (!arg.startsWith("-") && !arg.includes("bun")) {
|
||||||
|
// This is likely the URL
|
||||||
|
if (!arg.startsWith("bun") && !arg.includes("node")) {
|
||||||
|
args.url = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
176
src/cli/downloader.ts
Normal file
176
src/cli/downloader.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { spawn } from "child_process";
|
||||||
|
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
export interface DownloadOptions {
|
||||||
|
url: string;
|
||||||
|
outputDir: string;
|
||||||
|
format: string;
|
||||||
|
extractChapters: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Chapter {
|
||||||
|
title: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getVideoInfo(url: string): Promise<{ title: string; chapters: Chapter[] }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ytDlp = spawn("yt-dlp", [
|
||||||
|
"--dump-json",
|
||||||
|
"--no-download",
|
||||||
|
url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
ytDlp.stdout.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
ytDlp.stderr.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
ytDlp.on("close", (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(new Error(`yt-dlp failed: ${stderr}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = JSON.parse(stdout);
|
||||||
|
const chapters: Chapter[] = [];
|
||||||
|
|
||||||
|
if (info.chapters && Array.isArray(info.chapters)) {
|
||||||
|
for (const chapter of info.chapters) {
|
||||||
|
chapters.push({
|
||||||
|
title: chapter.title || `Chapter ${chapters.length + 1}`,
|
||||||
|
start: chapter.start_time || 0,
|
||||||
|
end: chapter.end_time || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
title: info.title || "video",
|
||||||
|
chapters,
|
||||||
|
});
|
||||||
|
} catch (parseError) {
|
||||||
|
reject(new Error(`Failed to parse video info: ${parseError}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ytDlp.on("error", (err) => {
|
||||||
|
reject(new Error(`Failed to run yt-dlp: ${err.message}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadSection(
|
||||||
|
url: string,
|
||||||
|
outputPath: string,
|
||||||
|
section: string,
|
||||||
|
format: string
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ytDlp = spawn("yt-dlp", [
|
||||||
|
"-f", format,
|
||||||
|
"--download-sections", section,
|
||||||
|
"-o", outputPath,
|
||||||
|
url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
ytDlp.stderr.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
ytDlp.on("close", (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(new Error(`yt-dlp failed: ${stderr}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
ytDlp.on("error", (err) => {
|
||||||
|
reject(new Error(`Failed to run yt-dlp: ${err.message}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeFilename(filename: string): string {
|
||||||
|
return filename
|
||||||
|
.replace(/[^a-zA-Z0-9\s\-_]/g, "")
|
||||||
|
.replace(/\s+/g, "_")
|
||||||
|
.substring(0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(seconds: number): string {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadVideoSegments(options: DownloadOptions): Promise<void> {
|
||||||
|
const { url, outputDir, format } = options;
|
||||||
|
|
||||||
|
// Create output directory if it doesn't exist
|
||||||
|
if (!existsSync(outputDir)) {
|
||||||
|
mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip chapter extraction if not requested
|
||||||
|
if (!options.extractChapters) {
|
||||||
|
console.log("Downloading full video (chapters disabled)...");
|
||||||
|
const safeTitle = sanitizeFilename("video");
|
||||||
|
const outputPath = join(outputDir, `${safeTitle}.%(ext)s`);
|
||||||
|
await downloadSection(url, outputPath, "*", format);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get video info including chapters
|
||||||
|
console.log("Fetching video information...");
|
||||||
|
const { title, chapters } = await getVideoInfo(url);
|
||||||
|
const safeTitle = sanitizeFilename(title);
|
||||||
|
|
||||||
|
console.log(`Video: ${title}`);
|
||||||
|
console.log(`Found ${chapters.length} chapters\n`);
|
||||||
|
|
||||||
|
if (chapters.length === 0) {
|
||||||
|
console.log("No chapters found. Downloading full video...");
|
||||||
|
const outputPath = join(outputDir, `${safeTitle}.%(ext)s`);
|
||||||
|
await downloadSection(url, outputPath, "*", format);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download each chapter as a separate segment
|
||||||
|
for (let i = 0; i < chapters.length; i++) {
|
||||||
|
const chapter = chapters[i];
|
||||||
|
const segmentNumber = (i + 1).toString().padStart(2, "0");
|
||||||
|
const outputPath = join(outputDir, `${safeTitle}_${segmentNumber}_${sanitizeFilename(chapter.title)}.%(ext)s`);
|
||||||
|
const section = `*${formatTime(chapter.start)}-${formatTime(chapter.end)}`;
|
||||||
|
|
||||||
|
console.log(`[${i + 1}/${chapters.length}] Downloading: ${chapter.title} (${formatTime(chapter.end - chapter.start)})`);
|
||||||
|
|
||||||
|
await downloadSection(url, outputPath, section, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also download the full video
|
||||||
|
console.log("\nDownloading full video...");
|
||||||
|
const fullVideoPath = join(outputDir, `${safeTitle}_full.%(ext)s`);
|
||||||
|
await downloadSection(url, fullVideoPath, "*", format);
|
||||||
|
|
||||||
|
// Save chapter list
|
||||||
|
const chapterListPath = join(outputDir, `${safeTitle}_chapters.txt`);
|
||||||
|
let chapterList = `# ${title}\n\n`;
|
||||||
|
for (const chapter of chapters) {
|
||||||
|
chapterList += `${formatTime(chapter.start)} - ${chapter.title}\n`;
|
||||||
|
}
|
||||||
|
writeFileSync(chapterListPath, chapterList);
|
||||||
|
console.log(`Chapter list saved to: ${chapterListPath}`);
|
||||||
|
}
|
||||||
40
src/cli/index.ts
Normal file
40
src/cli/index.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { downloadVideoSegments } from "./downloader.js";
|
||||||
|
import { parseArgs } from "./args.js";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = parseArgs();
|
||||||
|
|
||||||
|
if (!args.url) {
|
||||||
|
console.error("Error: YouTube URL is required");
|
||||||
|
console.log("Usage: yt-segments <url> [options]");
|
||||||
|
console.log("Options:");
|
||||||
|
console.log(" -o, --output <dir> Output directory (default: ./downloads)");
|
||||||
|
console.log(" -f, --format <fmt> Video format (default: best)");
|
||||||
|
console.log(" --no-chapters Skip chapter extraction");
|
||||||
|
console.log(" -h, --help Show help");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Downloading segments from: ${args.url}`);
|
||||||
|
console.log(`Output directory: ${args.output}`);
|
||||||
|
console.log(`Format: ${args.format}`);
|
||||||
|
console.log(`Extract chapters: ${!args.noChapters}`);
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadVideoSegments({
|
||||||
|
url: args.url,
|
||||||
|
outputDir: args.output,
|
||||||
|
format: args.format,
|
||||||
|
extractChapters: !args.noChapters,
|
||||||
|
});
|
||||||
|
console.log("\nDownload complete!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error instanceof Error ? error.message : error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
Reference in New Issue
Block a user