@mekuru/youtube 0.1.2 → 0.1.3

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": "@mekuru/youtube",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Uploads a video of Mekuru Movie Maker to YouTube with the title, description, ... written in youtube.yaml",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,6 +13,7 @@
13
13
  "bun": ">=1.4"
14
14
  },
15
15
  "dependencies": {
16
- "commander": "^14.0.3"
16
+ "commander": "^14.0.3",
17
+ "@mekuru/platform": "^0.1.3"
17
18
  }
18
19
  }
package/src/auth.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
- import { homedir } from "node:os";
2
+ import { configDir } from "@mekuru/platform";
3
+ export { configDir } from "@mekuru/platform";
3
4
  import { join } from "node:path";
4
5
 
5
6
  const SCOPE = "https://www.googleapis.com/auth/youtube.upload";
@@ -12,12 +13,6 @@ interface Client {
12
13
  token_uri: string;
13
14
  }
14
15
 
15
- export const configDir = (): string => {
16
- if (process.env.MEKURU_CONFIG_DIR) return process.env.MEKURU_CONFIG_DIR;
17
- if (process.platform === "win32") return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "mekuru");
18
- return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "mekuru");
19
- };
20
-
21
16
  const readClient = async (file: string): Promise<Client> => {
22
17
  if (!(await Bun.file(file).exists())) {
23
18
  throw new Error(`no OAuth client: save the JSON of the OAuth client (a desktop app) of your Google Cloud project as ${file}`);
package/src/cli.ts CHANGED
@@ -1,81 +1,7 @@
1
1
  #!/usr/bin/env bun
2
-
3
- import { existsSync } from "node:fs";
4
- import { resolve } from "node:path";
5
2
  import { Command } from "commander";
6
- import { accessToken, configDir } from "./auth.ts";
7
- import { readSettings, videoResource, type VideoResource } from "./settings.ts";
8
- import { setThumbnail, uploadVideo } from "./upload.ts";
9
-
3
+ import { main, DEFAULT } from "./command.ts";
10
4
  const VERSION: string = (await Bun.file(new URL("../package.json", import.meta.url)).json()).version;
11
- const DEFAULT = "configs/youtube.yaml";
12
-
13
- const openBrowser = (url: string): void => {
14
- const wsl = process.platform === "linux" && (!!process.env.WSL_INTEROP || existsSync("/proc/sys/fs/binfmt_misc/WSLInterop"));
15
- const tries: string[][] = wsl
16
- ? [["wslview"], ["cmd.exe", "/c", "start", ""]]
17
- : process.platform === "darwin"
18
- ? [["open"]]
19
- : process.platform === "win32"
20
- ? [["cmd", "/c", "start", ""]]
21
- : [["xdg-open"]];
22
- for (const [cmd, ...args] of tries) {
23
- const target = cmd.startsWith("cmd") ? url.replaceAll("&", "^&") : url;
24
- try {
25
- Bun.spawn([cmd, ...args, target], { stdin: "ignore", stdout: "ignore", stderr: "ignore", cwd: wsl ? "/mnt/c" : undefined }).unref();
26
- break;
27
- } catch {}
28
- }
29
- };
30
-
31
- const main = async (configs: string[], { dryRun = false }: { dryRun?: boolean }): Promise<void> => {
32
- const uploads = await Promise.all(
33
- (configs.length ? configs : [DEFAULT]).map(async (config) => {
34
- const settings = await readSettings(config);
35
- const file = resolve(settings.video ?? "video.mp4");
36
- const thumbnail = settings.thumbnail === undefined ? undefined : resolve(settings.thumbnail);
37
- for (const f of [file, thumbnail]) if (f && !(await Bun.file(f).exists())) throw new Error(`${f} does not exist (in ${config})`);
38
- return { config, file, thumbnail, video: videoResource(settings) };
39
- }),
40
- );
41
- if (dryRun) {
42
- for (const { config, file, thumbnail, video } of uploads) console.log(JSON.stringify({ config, file, thumbnail, ...video }, null, 2));
43
- console.log(`(the sign-in is kept in ${configDir()})`);
44
- return;
45
- }
46
-
47
- const token = await accessToken(openBrowser);
48
- for (const { config, file, thumbnail, video } of uploads) {
49
- if (uploads.length > 1) console.log(`${config}:`);
50
- await uploadOne(file, thumbnail, video, token);
51
- }
52
- };
53
-
54
- const uploadOne = async (file: string, thumbnail: string | undefined, video: VideoResource, token: string): Promise<void> => {
55
- const tty = process.stderr.isTTY;
56
- const uploaded = await uploadVideo(file, video, token, {
57
- progress: (sent, total) =>
58
- tty && process.stderr.write(`\ruploading ${Math.floor((sent / total) * 100)}% (${(sent / 2 ** 20).toFixed(1)} / ${(total / 2 ** 20).toFixed(1)} MiB)`),
59
- });
60
- if (tty) process.stderr.write("\n");
61
- console.log(`uploaded: https://youtu.be/${uploaded.id}`);
62
- console.log(`edit: https://studio.youtube.com/video/${uploaded.id}/edit`);
63
- const privacy = uploaded.status?.privacyStatus;
64
- if (privacy && privacy !== video.status.privacyStatus) {
65
- console.error(
66
- `note: the video is ${privacy}, not ${video.status.privacyStatus} (YouTube makes the uploads of an API project that has not passed its audit private: change it in YouTube Studio)`,
67
- );
68
- }
69
- if (thumbnail) {
70
- try {
71
- await setThumbnail(uploaded.id, thumbnail, token);
72
- } catch (e) {
73
- console.error(`warning: ${(e as Error).message}`);
74
- process.exitCode = 1;
75
- }
76
- }
77
- };
78
-
79
5
  new Command("mekuru-youtube")
80
6
  .description(
81
7
  "Uploads videos to YouTube, one for each YAML file with its settings: the video file, the title, description, tags, ... " +
package/src/command.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { openBrowser } from "@mekuru/platform";
2
+ import { resolve } from "node:path";
3
+ import { accessToken, configDir } from "./auth.ts";
4
+ import { readSettings, videoResource, type VideoResource } from "./settings.ts";
5
+ import { setThumbnail, uploadVideo } from "./upload.ts";
6
+
7
+ export const DEFAULT = "configs/youtube.yaml";
8
+
9
+ export const main = async (configs: string[], { dryRun = false }: { dryRun?: boolean }): Promise<void> => {
10
+ const uploads = await Promise.all(
11
+ (configs.length ? configs : [DEFAULT]).map(async (config) => {
12
+ const settings = await readSettings(config);
13
+ const file = resolve(settings.video ?? "video.mp4");
14
+ const thumbnail = settings.thumbnail === undefined ? undefined : resolve(settings.thumbnail);
15
+ for (const f of [file, thumbnail]) if (f && !(await Bun.file(f).exists())) throw new Error(`${f} does not exist (in ${config})`);
16
+ return { config, file, thumbnail, video: videoResource(settings) };
17
+ }),
18
+ );
19
+ if (dryRun) {
20
+ for (const { config, file, thumbnail, video } of uploads) console.log(JSON.stringify({ config, file, thumbnail, ...video }, null, 2));
21
+ console.log(`(the sign-in is kept in ${configDir()})`);
22
+ return;
23
+ }
24
+
25
+ const token = await accessToken(openBrowser);
26
+ for (const { config, file, thumbnail, video } of uploads) {
27
+ if (uploads.length > 1) console.log(`${config}:`);
28
+ await uploadOne(file, thumbnail, video, token);
29
+ }
30
+ };
31
+
32
+ const uploadOne = async (file: string, thumbnail: string | undefined, video: VideoResource, token: string): Promise<void> => {
33
+ const tty = process.stderr.isTTY;
34
+ const uploaded = await uploadVideo(file, video, token, {
35
+ progress: (sent, total) =>
36
+ tty && process.stderr.write(`\ruploading ${Math.floor((sent / total) * 100)}% (${(sent / 2 ** 20).toFixed(1)} / ${(total / 2 ** 20).toFixed(1)} MiB)`),
37
+ });
38
+ if (tty) process.stderr.write("\n");
39
+ console.log(`uploaded: https://youtu.be/${uploaded.id}`);
40
+ console.log(`edit: https://studio.youtube.com/video/${uploaded.id}/edit`);
41
+ const privacy = uploaded.status?.privacyStatus;
42
+ if (privacy && privacy !== video.status.privacyStatus) {
43
+ console.error(
44
+ `note: the video is ${privacy}, not ${video.status.privacyStatus} (YouTube makes the uploads of an API project that has not passed its audit private: change it in YouTube Studio)`,
45
+ );
46
+ }
47
+ if (thumbnail) {
48
+ try {
49
+ await setThumbnail(uploaded.id, thumbnail, token);
50
+ } catch (e) {
51
+ console.error(`warning: ${(e as Error).message}`);
52
+ process.exitCode = 1;
53
+ }
54
+ }
55
+ };