comfyclips 1.1.0 → 1.3.0
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.
- package/README.md +8 -7
- package/package.json +15 -4
- package/src/binaries.js +18 -3
- package/src/downloader.js +26 -2
- package/src/index.js +3 -2
- package/src/platforms.js +38 -3
package/README.md
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# ComfyClips
|
|
2
2
|
|
|
3
|
-
Interactive CLI to download video or audio from social media links (YouTube, Instagram, TikTok, Facebook, X/Twitter). Built on top of [yt-dlp](https://github.com/yt-dlp/yt-dlp).
|
|
3
|
+
Interactive CLI to download video or audio from social media links (YouTube, Instagram, TikTok, Reddit, Pinterest, Vimeo, Dailymotion, Rumble, LinkedIn, Snapchat, Facebook, X/Twitter). Built on top of [yt-dlp](https://github.com/yt-dlp/yt-dlp).
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
- Node.js >= 18
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
8
|
+
|
|
9
|
+
That's it — everything else is provisioned automatically:
|
|
10
|
+
|
|
11
|
+
- **ffmpeg** — used to merge separate video/audio streams and to extract/convert audio formats. If it's not on your PATH, `npm install` fetches a static build for your platform (via `ffmpeg-static`).
|
|
12
|
+
- **yt-dlp** — if it isn't found on your PATH, ComfyClips downloads a copy on first run into `~/.comfyclips/bin`.
|
|
13
|
+
- **Deno** — YouTube requires a JS runtime to solve its signature challenge. If it isn't found on your PATH, ComfyClips downloads it on first run into `~/.comfyclips/bin`.
|
|
13
14
|
|
|
14
15
|
## Install
|
|
15
16
|
|
|
@@ -25,7 +26,7 @@ comfyclips
|
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
You'll be prompted to:
|
|
28
|
-
1. Select a platform (YouTube, Instagram,
|
|
29
|
+
1. Select a platform (YouTube, TikTok, Instagram, Reddit, Pinterest, Vimeo, Dailymotion, Rumble, LinkedIn, Snapchat, Facebook, X/Twitter)
|
|
29
30
|
2. Paste the video link
|
|
30
31
|
3. Choose Video or Audio only
|
|
31
32
|
4. Pick a quality (video) or format (audio: mp3/m4a/wav/opus)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comfyclips",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Interactive CLI to download video or audio from social media platforms (YouTube, Instagram,
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Interactive CLI to download video or audio from social media platforms (YouTube, TikTok, Instagram, Reddit, Pinterest, Vimeo, Dailymotion, Rumble, LinkedIn, Snapchat, Facebook, X/Twitter) via yt-dlp.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"comfyclips": "./src/index.js"
|
|
@@ -13,15 +13,25 @@
|
|
|
13
13
|
"LICENSE"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"start": "node src/index.js"
|
|
16
|
+
"start": "node src/index.js",
|
|
17
|
+
"test": "node --test test/**/*.test.js"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
|
19
20
|
"cli",
|
|
20
21
|
"video-downloader",
|
|
21
22
|
"yt-dlp",
|
|
22
23
|
"youtube",
|
|
24
|
+
"tiktok",
|
|
25
|
+
"reddit",
|
|
26
|
+
"pinterest",
|
|
27
|
+
"vimeo",
|
|
23
28
|
"instagram",
|
|
24
|
-
"
|
|
29
|
+
"twitter",
|
|
30
|
+
"facebook",
|
|
31
|
+
"dailymotion",
|
|
32
|
+
"rumble",
|
|
33
|
+
"linkedin",
|
|
34
|
+
"snapchat"
|
|
25
35
|
],
|
|
26
36
|
"author": "Hamza Imran",
|
|
27
37
|
"license": "ISC",
|
|
@@ -35,6 +45,7 @@
|
|
|
35
45
|
"chalk": "^5.3.0",
|
|
36
46
|
"cli-progress": "^3.12.0",
|
|
37
47
|
"cli-table3": "^0.6.5",
|
|
48
|
+
"ffmpeg-static": "^5.3.0",
|
|
38
49
|
"figlet": "^1.11.4",
|
|
39
50
|
"inquirer": "^14.1.0",
|
|
40
51
|
"ora": "^8.1.0",
|
package/src/binaries.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import AdmZip from 'adm-zip';
|
|
2
|
+
import ffmpegStaticPath from 'ffmpeg-static';
|
|
2
3
|
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { chmodSync, existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
4
5
|
import os from 'node:os';
|
|
@@ -7,7 +8,12 @@ import chalk from 'chalk';
|
|
|
7
8
|
import ora from 'ora';
|
|
8
9
|
import YTDlpWrap from './ytdlp-lib.js';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
// On Vercel (and other read-only-filesystem serverless hosts) only /tmp is
|
|
12
|
+
// writable, and it isn't guaranteed to survive across invocations — so this
|
|
13
|
+
// falls back to os.tmpdir() there instead of the user's home directory.
|
|
14
|
+
const CACHE_DIR = process.env.VERCEL
|
|
15
|
+
? path.join(os.tmpdir(), 'comfyclips-bin')
|
|
16
|
+
: path.join(os.homedir(), '.comfyclips', 'bin');
|
|
11
17
|
const YT_DLP_BIN_NAME = os.platform() === 'win32' ? 'yt-dlp.exe' : 'yt-dlp';
|
|
12
18
|
const CACHED_YT_DLP_PATH = path.join(CACHE_DIR, YT_DLP_BIN_NAME);
|
|
13
19
|
const DENO_BIN_NAME = os.platform() === 'win32' ? 'deno.exe' : 'deno';
|
|
@@ -19,9 +25,18 @@ function commandExists(cmd, versionFlag = '--version') {
|
|
|
19
25
|
return !result.error && result.status === 0;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
// Prefer a system ffmpeg if the user already has one on PATH; otherwise fall
|
|
29
|
+
// back to the static binary `ffmpeg-static` already fetched via its own npm
|
|
30
|
+
// postinstall step, so no separate download step is needed here.
|
|
31
|
+
export function resolveFfmpeg() {
|
|
23
32
|
// ffmpeg uses single-dash flags (-version), unlike most CLIs.
|
|
24
|
-
|
|
33
|
+
if (commandExists('ffmpeg', '-version')) {
|
|
34
|
+
return { available: true, locationArgs: [] };
|
|
35
|
+
}
|
|
36
|
+
if (ffmpegStaticPath && existsSync(ffmpegStaticPath)) {
|
|
37
|
+
return { available: true, locationArgs: ['--ffmpeg-location', ffmpegStaticPath] };
|
|
38
|
+
}
|
|
39
|
+
return { available: false, locationArgs: [] };
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
function denoAssetName() {
|
package/src/downloader.js
CHANGED
|
@@ -34,9 +34,22 @@ export function buildArgs({
|
|
|
34
34
|
outputDir,
|
|
35
35
|
ffmpegAvailable,
|
|
36
36
|
jsRuntimeArgs = [],
|
|
37
|
+
ffmpegLocationArgs = [],
|
|
37
38
|
}) {
|
|
38
39
|
const outputTemplate = path.join(outputDir, '%(title)s.%(ext)s');
|
|
39
|
-
const args = [
|
|
40
|
+
const args = [
|
|
41
|
+
url,
|
|
42
|
+
'-o',
|
|
43
|
+
outputTemplate,
|
|
44
|
+
'--no-playlist',
|
|
45
|
+
'--newline',
|
|
46
|
+
// Some platforms (Facebook, Reddit, etc.) use the full post caption as the
|
|
47
|
+
// title, which can blow past the filesystem's filename length limit.
|
|
48
|
+
'--trim-filenames',
|
|
49
|
+
'150',
|
|
50
|
+
...jsRuntimeArgs,
|
|
51
|
+
...ffmpegLocationArgs,
|
|
52
|
+
];
|
|
40
53
|
const warnings = [];
|
|
41
54
|
|
|
42
55
|
if (mode === 'audio') {
|
|
@@ -47,7 +60,16 @@ export function buildArgs({
|
|
|
47
60
|
);
|
|
48
61
|
}
|
|
49
62
|
} else if (ffmpegAvailable) {
|
|
50
|
-
args.push(
|
|
63
|
+
args.push(
|
|
64
|
+
'-S',
|
|
65
|
+
'vcodec:h264,acodec:aac',
|
|
66
|
+
'-f',
|
|
67
|
+
videoFormatSelector(quality),
|
|
68
|
+
'--merge-output-format',
|
|
69
|
+
'mp4',
|
|
70
|
+
'--postprocessor-args',
|
|
71
|
+
'Merger:-c:a aac -movflags +faststart'
|
|
72
|
+
);
|
|
51
73
|
} else {
|
|
52
74
|
const h = heightCap(quality);
|
|
53
75
|
args.push('-f', `best[ext=mp4]${h}/best${h}`);
|
|
@@ -70,6 +92,7 @@ export async function downloadMedia({
|
|
|
70
92
|
outputDir,
|
|
71
93
|
ffmpegAvailable,
|
|
72
94
|
jsRuntimeArgs,
|
|
95
|
+
ffmpegLocationArgs,
|
|
73
96
|
}) {
|
|
74
97
|
mkdirSync(outputDir, { recursive: true });
|
|
75
98
|
|
|
@@ -82,6 +105,7 @@ export async function downloadMedia({
|
|
|
82
105
|
outputDir,
|
|
83
106
|
ffmpegAvailable,
|
|
84
107
|
jsRuntimeArgs,
|
|
108
|
+
ffmpegLocationArgs,
|
|
85
109
|
});
|
|
86
110
|
warnings.forEach(printWarning);
|
|
87
111
|
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync, statSync } from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
|
-
import {
|
|
6
|
+
import { resolveFfmpeg, resolveJsRuntimeArgs, resolveYtDlpBinaryPath } from './binaries.js';
|
|
7
7
|
import { downloadMedia } from './downloader.js';
|
|
8
8
|
import { PLATFORMS, hostMatchesPlatform } from './platforms.js';
|
|
9
9
|
import boxedSelect from './prompts/boxedSelect.js';
|
|
@@ -137,7 +137,7 @@ async function main() {
|
|
|
137
137
|
|
|
138
138
|
try {
|
|
139
139
|
const binaryPath = await resolveYtDlpBinaryPath();
|
|
140
|
-
const ffmpegAvailable =
|
|
140
|
+
const { available: ffmpegAvailable, locationArgs: ffmpegLocationArgs } = resolveFfmpeg();
|
|
141
141
|
const jsRuntimeArgs = await resolveJsRuntimeArgs();
|
|
142
142
|
|
|
143
143
|
const { outputFile, elapsedSeconds } = await downloadMedia({
|
|
@@ -149,6 +149,7 @@ async function main() {
|
|
|
149
149
|
outputDir,
|
|
150
150
|
ffmpegAvailable,
|
|
151
151
|
jsRuntimeArgs,
|
|
152
|
+
ffmpegLocationArgs,
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
let fileSize = 'Unknown';
|
package/src/platforms.js
CHANGED
|
@@ -15,15 +15,50 @@ export const PLATFORMS = [
|
|
|
15
15
|
hostPattern: /(^|\.)tiktok\.com$/i,
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
|
-
name: '
|
|
19
|
-
value: '
|
|
20
|
-
hostPattern: /(^|\.)
|
|
18
|
+
name: 'Reddit',
|
|
19
|
+
value: 'reddit',
|
|
20
|
+
hostPattern: /(^|\.)reddit\.com$|^v\.redd\.it$|^redd\.it$/i,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'Pinterest',
|
|
24
|
+
value: 'pinterest',
|
|
25
|
+
hostPattern: /(^|\.)pinterest\.[a-z.]+$|^pin\.it$/i,
|
|
21
26
|
},
|
|
22
27
|
{
|
|
23
28
|
name: 'X / Twitter',
|
|
24
29
|
value: 'twitter',
|
|
25
30
|
hostPattern: /(^|\.)twitter\.com$|(^|\.)x\.com$/i,
|
|
26
31
|
},
|
|
32
|
+
{
|
|
33
|
+
name: 'Facebook',
|
|
34
|
+
value: 'facebook',
|
|
35
|
+
hostPattern: /(^|\.)facebook\.com$|^fb\.watch$/i,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'Vimeo',
|
|
39
|
+
value: 'vimeo',
|
|
40
|
+
hostPattern: /(^|\.)vimeo\.com$/i,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Dailymotion',
|
|
44
|
+
value: 'dailymotion',
|
|
45
|
+
hostPattern: /(^|\.)dailymotion\.com$|^dai\.ly$/i,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Rumble',
|
|
49
|
+
value: 'rumble',
|
|
50
|
+
hostPattern: /(^|\.)rumble\.com$/i,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'LinkedIn',
|
|
54
|
+
value: 'linkedin',
|
|
55
|
+
hostPattern: /(^|\.)linkedin\.com$|^lnkd\.in$/i,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'Snapchat',
|
|
59
|
+
value: 'snapchat',
|
|
60
|
+
hostPattern: /(^|\.)snapchat\.com$/i,
|
|
61
|
+
},
|
|
27
62
|
];
|
|
28
63
|
|
|
29
64
|
export function hostMatchesPlatform(url, platformValue) {
|