comfyclips 1.0.0 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comfyclips",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Interactive CLI to download video or audio from social media platforms (YouTube, Instagram, TikTok, Facebook, X/Twitter) via yt-dlp.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,6 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@inquirer/core": "^12.0.0",
33
+ "adm-zip": "^0.6.0",
33
34
  "boxen": "^8.0.1",
34
35
  "chalk": "^5.3.0",
35
36
  "cli-progress": "^3.12.0",
@@ -38,6 +39,5 @@
38
39
  "inquirer": "^14.1.0",
39
40
  "ora": "^8.1.0",
40
41
  "yt-dlp-wrap-plus": "^2.5.0"
41
- },
42
- "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c"
42
+ }
43
43
  }
package/src/binaries.js CHANGED
@@ -1,5 +1,6 @@
1
+ import AdmZip from 'adm-zip';
1
2
  import { spawnSync } from 'node:child_process';
2
- import { existsSync, mkdirSync } from 'node:fs';
3
+ import { chmodSync, existsSync, mkdirSync, rmSync } from 'node:fs';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
5
6
  import chalk from 'chalk';
@@ -9,6 +10,9 @@ import YTDlpWrap from './ytdlp-lib.js';
9
10
  const CACHE_DIR = path.join(os.homedir(), '.comfyclips', 'bin');
10
11
  const YT_DLP_BIN_NAME = os.platform() === 'win32' ? 'yt-dlp.exe' : 'yt-dlp';
11
12
  const CACHED_YT_DLP_PATH = path.join(CACHE_DIR, YT_DLP_BIN_NAME);
13
+ const DENO_BIN_NAME = os.platform() === 'win32' ? 'deno.exe' : 'deno';
14
+ const CACHED_DENO_PATH = path.join(CACHE_DIR, DENO_BIN_NAME);
15
+ const DENO_LATEST_RELEASE_API = 'https://api.github.com/repos/denoland/deno/releases/latest';
12
16
 
13
17
  function commandExists(cmd, versionFlag = '--version') {
14
18
  const result = spawnSync(cmd, [versionFlag], { stdio: 'ignore' });
@@ -20,6 +24,65 @@ export function isFfmpegAvailable() {
20
24
  return commandExists('ffmpeg', '-version');
21
25
  }
22
26
 
27
+ function denoAssetName() {
28
+ const platform = os.platform();
29
+ const arch = os.arch() === 'arm64' ? 'aarch64' : 'x86_64';
30
+
31
+ if (platform === 'darwin') return `deno-${arch}-apple-darwin.zip`;
32
+ if (platform === 'win32') return `deno-${arch}-pc-windows-msvc.zip`;
33
+ return `deno-${arch}-unknown-linux-gnu.zip`;
34
+ }
35
+
36
+ async function downloadDeno(destPath) {
37
+ const release = await fetch(DENO_LATEST_RELEASE_API).then((res) => {
38
+ if (!res.ok) throw new Error(`GitHub API responded with ${res.status}`);
39
+ return res.json();
40
+ });
41
+
42
+ const assetName = denoAssetName();
43
+ const asset = release.assets?.find((a) => a.name === assetName);
44
+ if (!asset) throw new Error(`No Deno release asset found for ${assetName}`);
45
+
46
+ const zipBuffer = await fetch(asset.browser_download_url).then((res) => {
47
+ if (!res.ok) throw new Error(`Download responded with ${res.status}`);
48
+ return res.arrayBuffer();
49
+ });
50
+
51
+ const zip = new AdmZip(Buffer.from(zipBuffer));
52
+ const entry = zip.getEntries().find((e) => e.entryName === DENO_BIN_NAME);
53
+ if (!entry) throw new Error(`${DENO_BIN_NAME} not found inside downloaded archive`);
54
+
55
+ zip.extractEntryTo(entry, CACHE_DIR, false, true);
56
+ if (os.platform() !== 'win32') chmodSync(destPath, 0o755);
57
+ }
58
+
59
+ // YouTube's extractor needs a JS runtime (Deno) to solve its signature
60
+ // challenge; without one, most modern formats silently disappear from the
61
+ // list yt-dlp reports. Auto-provision it the same way we do for yt-dlp
62
+ // itself, so `npm install` alone is enough — no manual setup step.
63
+ export async function resolveJsRuntimeArgs() {
64
+ if (commandExists('deno', '--version')) {
65
+ return [];
66
+ }
67
+
68
+ if (existsSync(CACHED_DENO_PATH)) {
69
+ return ['--js-runtimes', `deno:${CACHED_DENO_PATH}`];
70
+ }
71
+
72
+ console.log(chalk.yellow('No JS runtime found — required for reliable YouTube downloads.'));
73
+ const spinner = ora('Downloading Deno runtime (one-time setup)...').start();
74
+ try {
75
+ mkdirSync(CACHE_DIR, { recursive: true });
76
+ await downloadDeno(CACHED_DENO_PATH);
77
+ spinner.succeed(`Deno downloaded to ${CACHED_DENO_PATH}`);
78
+ return ['--js-runtimes', `deno:${CACHED_DENO_PATH}`];
79
+ } catch (err) {
80
+ spinner.fail(`Failed to download Deno automatically: ${err.message}`);
81
+ rmSync(CACHED_DENO_PATH, { force: true });
82
+ return [];
83
+ }
84
+ }
85
+
23
86
  export async function resolveYtDlpBinaryPath() {
24
87
  if (commandExists('yt-dlp')) {
25
88
  return 'yt-dlp';
package/src/downloader.js CHANGED
@@ -26,9 +26,17 @@ function videoFormatSelector(quality) {
26
26
  ].join('/');
27
27
  }
28
28
 
29
- export function buildArgs({ url, mode, quality, audioFormat, outputDir, ffmpegAvailable }) {
29
+ export function buildArgs({
30
+ url,
31
+ mode,
32
+ quality,
33
+ audioFormat,
34
+ outputDir,
35
+ ffmpegAvailable,
36
+ jsRuntimeArgs = [],
37
+ }) {
30
38
  const outputTemplate = path.join(outputDir, '%(title)s.%(ext)s');
31
- const args = [url, '-o', outputTemplate, '--no-playlist', '--newline'];
39
+ const args = [url, '-o', outputTemplate, '--no-playlist', '--newline', ...jsRuntimeArgs];
32
40
  const warnings = [];
33
41
 
34
42
  if (mode === 'audio') {
@@ -61,6 +69,7 @@ export async function downloadMedia({
61
69
  audioFormat,
62
70
  outputDir,
63
71
  ffmpegAvailable,
72
+ jsRuntimeArgs,
64
73
  }) {
65
74
  mkdirSync(outputDir, { recursive: true });
66
75
 
@@ -72,6 +81,7 @@ export async function downloadMedia({
72
81
  audioFormat,
73
82
  outputDir,
74
83
  ffmpegAvailable,
84
+ jsRuntimeArgs,
75
85
  });
76
86
  warnings.forEach(printWarning);
77
87
 
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 { isFfmpegAvailable, resolveYtDlpBinaryPath } from './binaries.js';
6
+ import { isFfmpegAvailable, 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';
@@ -138,6 +138,7 @@ async function main() {
138
138
  try {
139
139
  const binaryPath = await resolveYtDlpBinaryPath();
140
140
  const ffmpegAvailable = isFfmpegAvailable();
141
+ const jsRuntimeArgs = await resolveJsRuntimeArgs();
141
142
 
142
143
  const { outputFile, elapsedSeconds } = await downloadMedia({
143
144
  binaryPath,
@@ -147,6 +148,7 @@ async function main() {
147
148
  audioFormat,
148
149
  outputDir,
149
150
  ffmpegAvailable,
151
+ jsRuntimeArgs,
150
152
  });
151
153
 
152
154
  let fileSize = 'Unknown';