@pagepocket/plugin-yt-dlp 0.8.5 → 0.9.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/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { default } from "./yt-dlp-plugin.js";
2
2
  export * from "./yt-dlp-plugin.js";
3
+ export { setup } from "./setup.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { default } from "./yt-dlp-plugin.js";
2
2
  export * from "./yt-dlp-plugin.js";
3
+ export { setup } from "./setup.js";
@@ -0,0 +1,6 @@
1
+ export type BinaryPathJson = {
2
+ "yt-dlp": string;
3
+ ffmpeg: string;
4
+ };
5
+ export declare const resolveBinaryPathJsonFilePath: () => string;
6
+ export declare const setup: () => Promise<void>;
package/dist/setup.js ADDED
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { helpers } from "ytdlp-nodejs";
5
+ import { findExecutable } from "./utils/find-executable.js";
6
+ const findNearestDirWithPackageJson = (startDir) => {
7
+ let dir = startDir;
8
+ for (;;) {
9
+ const pkgJsonPath = path.join(dir, "package.json");
10
+ if (fs.existsSync(pkgJsonPath)) {
11
+ return dir;
12
+ }
13
+ const parent = path.dirname(dir);
14
+ if (parent === dir) {
15
+ return startDir;
16
+ }
17
+ dir = parent;
18
+ }
19
+ };
20
+ const writeJsonAtomic = async (filePath, value) => {
21
+ const dir = path.dirname(filePath);
22
+ await fs.promises.mkdir(dir, { recursive: true });
23
+ const tmpPath = `${filePath}.tmp`;
24
+ await fs.promises.writeFile(tmpPath, `${JSON.stringify(value, undefined, 2)}\n`, "utf8");
25
+ await fs.promises.rename(tmpPath, filePath);
26
+ };
27
+ export const resolveBinaryPathJsonFilePath = () => {
28
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
29
+ const packageRoot = findNearestDirWithPackageJson(thisDir);
30
+ return path.join(packageRoot, "binary-path.json");
31
+ };
32
+ export const setup = async () => {
33
+ console.info("Checking yt-dlp/ffmpeg on PATH...");
34
+ const ytdlpPath = await findExecutable("yt-dlp");
35
+ const ffmpegPath = await findExecutable("ffmpeg");
36
+ const outPath = resolveBinaryPathJsonFilePath();
37
+ if (ytdlpPath && ffmpegPath) {
38
+ console.info("Found yt-dlp/ffmpeg; writing binary-path.json");
39
+ const json = { "yt-dlp": ytdlpPath, ffmpeg: ffmpegPath };
40
+ await writeJsonAtomic(outPath, json);
41
+ return;
42
+ }
43
+ console.info("yt-dlp/ffmpeg not found; downloading via ytdlp-nodejs...");
44
+ const downloadedYtdlpPath = await helpers.downloadYtDlp();
45
+ const downloadedFfmpegPath = await helpers.downloadFFmpeg();
46
+ const resolvedFfmpegPath = downloadedFfmpegPath ?? helpers.findFFmpegBinary();
47
+ if (!resolvedFfmpegPath) {
48
+ throw new Error("Failed to download or locate ffmpeg via ytdlp-nodejs helpers.");
49
+ }
50
+ const json = { "yt-dlp": downloadedYtdlpPath, ffmpeg: resolvedFfmpegPath };
51
+ console.info("Download complete; writing binary-path.json");
52
+ await writeJsonAtomic(outPath, json);
53
+ };
@@ -0,0 +1,7 @@
1
+ type FindExecutableOptions = {
2
+ pathEnv?: string;
3
+ pathExtEnv?: string;
4
+ cwd?: string;
5
+ };
6
+ export declare const findExecutable: (command: string, options?: FindExecutableOptions) => Promise<string | undefined>;
7
+ export {};
@@ -0,0 +1,69 @@
1
+ import nodeFs from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ const hasPathSeparator = (value) => value.includes("/") || value.includes("\\");
5
+ const getWindowsPathExts = (pathExtEnv) => {
6
+ const raw = (pathExtEnv ?? process.env.PATHEXT ?? "").trim();
7
+ const defaults = [".COM", ".EXE", ".BAT", ".CMD"];
8
+ if (!raw) {
9
+ return defaults;
10
+ }
11
+ const extList = raw
12
+ .split(";")
13
+ .map((s) => s.trim())
14
+ .filter(Boolean)
15
+ .map((s) => (s.startsWith(".") ? s : `.${s}`));
16
+ return extList.length > 0 ? extList : defaults;
17
+ };
18
+ const isExecutableFile = async (absPath) => {
19
+ try {
20
+ const stat = await fs.stat(absPath);
21
+ if (!stat.isFile()) {
22
+ return false;
23
+ }
24
+ if (process.platform === "win32") {
25
+ return true;
26
+ }
27
+ await fs.access(absPath, nodeFs.constants.X_OK);
28
+ return true;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ };
34
+ const buildCandidateNames = (command, options) => {
35
+ if (process.platform !== "win32") {
36
+ return [command];
37
+ }
38
+ if (path.extname(command)) {
39
+ return [command];
40
+ }
41
+ const exts = getWindowsPathExts(options.pathExtEnv);
42
+ return exts.map((ext) => `${command}${ext.toLowerCase()}`);
43
+ };
44
+ export const findExecutable = async (command, options = {}) => {
45
+ const trimmed = command.trim();
46
+ if (!trimmed) {
47
+ return undefined;
48
+ }
49
+ const cwd = options.cwd ?? process.cwd();
50
+ if (hasPathSeparator(trimmed)) {
51
+ const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
52
+ return (await isExecutableFile(abs)) ? abs : undefined;
53
+ }
54
+ const pathEnv = options.pathEnv ?? process.env.PATH ?? "";
55
+ const dirs = pathEnv
56
+ .split(path.delimiter)
57
+ .map((s) => s.trim())
58
+ .filter(Boolean);
59
+ const candidateNames = buildCandidateNames(trimmed, options);
60
+ for (const dir of dirs) {
61
+ for (const name of candidateNames) {
62
+ const abs = path.join(dir, name);
63
+ if (await isExecutableFile(abs)) {
64
+ return abs;
65
+ }
66
+ }
67
+ }
68
+ return undefined;
69
+ };
@@ -10,7 +10,7 @@ export type YoutubeJob = {
10
10
  export declare const parseYoutubeEmbedSrc: (src: string) => {
11
11
  id: string;
12
12
  url: string;
13
- } | null;
13
+ } | undefined;
14
14
  export declare class YtDlpJobManager {
15
15
  createSetupValue(now?: Date): SetupValue;
16
16
  buildVideoRelPath(setupValue: SetupValue, videoId: string): string;
@@ -4,21 +4,21 @@ export const parseYoutubeEmbedSrc = (src) => {
4
4
  const url = new URL(src, "https://www.youtube.com");
5
5
  const host = url.hostname;
6
6
  if (!host.endsWith("youtube.com") && !host.endsWith("youtube-nocookie.com")) {
7
- return null;
7
+ return undefined;
8
8
  }
9
9
  const parts = url.pathname.split("/").filter(Boolean);
10
10
  const embedIndex = parts.indexOf("embed");
11
11
  if (embedIndex === -1) {
12
- return null;
12
+ return undefined;
13
13
  }
14
14
  const id = parts[embedIndex + 1];
15
15
  if (!id) {
16
- return null;
16
+ return undefined;
17
17
  }
18
18
  return { id, url: `https://www.youtube.com/watch?v=${id}` };
19
19
  }
20
20
  catch {
21
- return null;
21
+ return undefined;
22
22
  }
23
23
  };
24
24
  const uniqueById = (jobs) => {
@@ -1,11 +1,50 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import pathMod from "node:path";
4
+ import { YtDlp, helpers } from "ytdlp-nodejs";
5
+ import { resolveBinaryPathJsonFilePath } from "../setup.js";
6
+ const isBinaryPathJson = (value) => {
7
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
8
+ return false;
9
+ }
10
+ const v = value;
11
+ return typeof v["yt-dlp"] === "string" && typeof v.ffmpeg === "string";
12
+ };
13
+ const readBinaryPathJson = async () => {
14
+ try {
15
+ const filePath = resolveBinaryPathJsonFilePath();
16
+ const text = await fs.readFile(filePath, "utf8");
17
+ const parsed = JSON.parse(text);
18
+ return isBinaryPathJson(parsed) ? parsed : undefined;
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ };
1
24
  export const downloadVideosAsBytes = async (jobs) => {
2
- const { YtDlp, helpers } = await import("ytdlp-nodejs");
3
- const fs = await import("node:fs/promises");
4
- const pathMod = await import("node:path");
5
- const os = await import("node:os");
6
- const ytdlp = new YtDlp();
7
- await helpers.downloadYtDlp();
8
- await helpers.downloadFFmpeg();
25
+ const binaryPathJson = await readBinaryPathJson();
26
+ if (!binaryPathJson) {
27
+ console.info("binary-path.json missing/invalid; downloading yt-dlp/ffmpeg via ytdlp-nodejs...");
28
+ await helpers.downloadYtDlp();
29
+ await helpers.downloadFFmpeg();
30
+ const resolvedYtdlpPath = helpers.findYtdlpBinary();
31
+ const resolvedFfmpegPath = helpers.findFFmpegBinary();
32
+ if (!resolvedYtdlpPath || !resolvedFfmpegPath) {
33
+ throw new Error("Failed to locate yt-dlp/ffmpeg after download.");
34
+ }
35
+ console.info("Binaries ready; starting yt-dlp downloads");
36
+ const ytdlp = new YtDlp({ binaryPath: resolvedYtdlpPath, ffmpegPath: resolvedFfmpegPath });
37
+ return await downloadWithYtdlp({ jobs, ytdlp, fs, pathMod, os });
38
+ }
39
+ const ytdlp = new YtDlp({
40
+ binaryPath: binaryPathJson["yt-dlp"],
41
+ ffmpegPath: binaryPathJson.ffmpeg
42
+ });
43
+ console.info("Using binary-path.json; starting yt-dlp downloads");
44
+ return await downloadWithYtdlp({ jobs, ytdlp, fs, pathMod, os });
45
+ };
46
+ const downloadWithYtdlp = async (input) => {
47
+ const { jobs, ytdlp, fs, pathMod, os } = input;
9
48
  const bytesById = new Map();
10
49
  await Promise.all(jobs.map(async (job) => {
11
50
  const tmpDir = await fs.mkdtemp(pathMod.join(os.tmpdir(), "pagepocket-ytdlp-"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagepocket/plugin-yt-dlp",
3
- "version": "0.8.5",
3
+ "version": "0.9.0",
4
4
  "description": "PagePocket plugin: download YouTube embeds and replace with <video>",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,11 +11,12 @@
11
11
  "license": "ISC",
12
12
  "dependencies": {
13
13
  "ytdlp-nodejs": "^3.4.2",
14
- "@pagepocket/shared": "0.8.5",
15
- "@pagepocket/contracts": "0.8.5",
16
- "@pagepocket/lib": "0.8.5"
14
+ "@pagepocket/lib": "0.9.0",
15
+ "@pagepocket/contracts": "0.9.0",
16
+ "@pagepocket/shared": "0.9.0"
17
17
  },
18
18
  "devDependencies": {
19
+ "@types/node": "^20.17.12",
19
20
  "typescript": "^5.4.5"
20
21
  },
21
22
  "scripts": {