@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 +1 -0
- package/dist/index.js +1 -0
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +53 -0
- package/dist/utils/find-executable.d.ts +7 -0
- package/dist/utils/find-executable.js +69 -0
- package/dist/utils/yt-dlp-job-manager.d.ts +1 -1
- package/dist/utils/yt-dlp-job-manager.js +4 -4
- package/dist/utils/ytdlp.js +46 -7
- package/package.json +5 -4
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/setup.d.ts
ADDED
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,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
|
-
} |
|
|
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
|
|
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
|
|
12
|
+
return undefined;
|
|
13
13
|
}
|
|
14
14
|
const id = parts[embedIndex + 1];
|
|
15
15
|
if (!id) {
|
|
16
|
-
return
|
|
16
|
+
return undefined;
|
|
17
17
|
}
|
|
18
18
|
return { id, url: `https://www.youtube.com/watch?v=${id}` };
|
|
19
19
|
}
|
|
20
20
|
catch {
|
|
21
|
-
return
|
|
21
|
+
return undefined;
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
24
|
const uniqueById = (jobs) => {
|
package/dist/utils/ytdlp.js
CHANGED
|
@@ -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
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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.
|
|
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/
|
|
15
|
-
"@pagepocket/contracts": "0.
|
|
16
|
-
"@pagepocket/
|
|
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": {
|