youtube-data-cli 0.0.1
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/LICENSE +201 -0
- package/README.md +424 -0
- package/dist/api.js +39 -0
- package/dist/auth.js +72 -0
- package/dist/commands/channels.js +29 -0
- package/dist/commands/comment-threads.js +73 -0
- package/dist/commands/comments.js +97 -0
- package/dist/commands/playlist-items.js +113 -0
- package/dist/commands/playlists.js +126 -0
- package/dist/commands/search.js +68 -0
- package/dist/commands/subscriptions.js +90 -0
- package/dist/commands/videos.js +23 -0
- package/dist/index.js +56 -0
- package/dist/utils.js +12 -0
- package/package.json +50 -0
package/dist/auth.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const DEFAULT_PATH = join(homedir(), ".config", "youtube-data-cli", "credentials.json");
|
|
5
|
+
export function loadCredentials(credentialsPath) {
|
|
6
|
+
// 1. --credentials flag
|
|
7
|
+
if (credentialsPath) {
|
|
8
|
+
return readJSON(credentialsPath);
|
|
9
|
+
}
|
|
10
|
+
// 2. Environment variables
|
|
11
|
+
const envCreds = {};
|
|
12
|
+
if (process.env.YOUTUBE_API_KEY)
|
|
13
|
+
envCreds.api_key = process.env.YOUTUBE_API_KEY;
|
|
14
|
+
if (process.env.YOUTUBE_CLIENT_ID)
|
|
15
|
+
envCreds.client_id = process.env.YOUTUBE_CLIENT_ID;
|
|
16
|
+
if (process.env.YOUTUBE_CLIENT_SECRET)
|
|
17
|
+
envCreds.client_secret = process.env.YOUTUBE_CLIENT_SECRET;
|
|
18
|
+
if (process.env.YOUTUBE_REFRESH_TOKEN)
|
|
19
|
+
envCreds.refresh_token = process.env.YOUTUBE_REFRESH_TOKEN;
|
|
20
|
+
if (Object.keys(envCreds).length > 0) {
|
|
21
|
+
return envCreds;
|
|
22
|
+
}
|
|
23
|
+
// 3. Default credentials file
|
|
24
|
+
if (existsSync(DEFAULT_PATH)) {
|
|
25
|
+
return readJSON(DEFAULT_PATH);
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`No credentials found. Provide one of:\n` +
|
|
28
|
+
` 1. --credentials <path> flag\n` +
|
|
29
|
+
` 2. YOUTUBE_API_KEY and/or YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET, YOUTUBE_REFRESH_TOKEN env vars\n` +
|
|
30
|
+
` 3. ${DEFAULT_PATH}`);
|
|
31
|
+
}
|
|
32
|
+
function readJSON(path) {
|
|
33
|
+
const raw = readFileSync(path, "utf-8");
|
|
34
|
+
const data = JSON.parse(raw);
|
|
35
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
36
|
+
throw new Error(`credentials file must be a JSON object: ${path}`);
|
|
37
|
+
}
|
|
38
|
+
if (!data.api_key && !data.client_id) {
|
|
39
|
+
throw new Error(`credentials file must contain at least "api_key" or "client_id": ${path}`);
|
|
40
|
+
}
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
let cachedAccessToken = null;
|
|
44
|
+
export async function getAccessToken(creds) {
|
|
45
|
+
if (!creds.client_id || !creds.client_secret || !creds.refresh_token) {
|
|
46
|
+
throw new Error("OAuth credentials required (client_id, client_secret, refresh_token). " +
|
|
47
|
+
"API key alone is not sufficient for this command.");
|
|
48
|
+
}
|
|
49
|
+
// Return cached token if still valid (with 60s buffer)
|
|
50
|
+
if (cachedAccessToken && Date.now() < cachedAccessToken.expiresAt - 60_000) {
|
|
51
|
+
return cachedAccessToken.token;
|
|
52
|
+
}
|
|
53
|
+
const res = await fetch("https://oauth2.googleapis.com/token", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
56
|
+
body: new URLSearchParams({
|
|
57
|
+
client_id: creds.client_id,
|
|
58
|
+
client_secret: creds.client_secret,
|
|
59
|
+
refresh_token: creds.refresh_token,
|
|
60
|
+
grant_type: "refresh_token",
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
const json = (await res.json());
|
|
64
|
+
if (!res.ok || !json.access_token) {
|
|
65
|
+
throw new Error(json.error_description ?? json.error ?? `Token refresh failed: HTTP ${res.status}`);
|
|
66
|
+
}
|
|
67
|
+
cachedAccessToken = {
|
|
68
|
+
token: json.access_token,
|
|
69
|
+
expiresAt: Date.now() + (json.expires_in ?? 3600) * 1000,
|
|
70
|
+
};
|
|
71
|
+
return json.access_token;
|
|
72
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { loadCredentials } from "../auth.js";
|
|
2
|
+
import { callApi } from "../api.js";
|
|
3
|
+
import { output, fatal } from "../utils.js";
|
|
4
|
+
export function registerChannelCommands(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("channels [channel-id]")
|
|
7
|
+
.description("Get channel details (omit ID for authenticated user's channel)")
|
|
8
|
+
.option("--part <parts>", "Parts to include", "snippet,statistics,contentDetails")
|
|
9
|
+
.action(async (channelId, opts) => {
|
|
10
|
+
try {
|
|
11
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
12
|
+
const params = {
|
|
13
|
+
part: opts.part,
|
|
14
|
+
};
|
|
15
|
+
const requireOAuth = !channelId;
|
|
16
|
+
if (channelId) {
|
|
17
|
+
params.id = channelId;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
params.mine = "true";
|
|
21
|
+
}
|
|
22
|
+
const data = await callApi("/channels", { creds, params, requireOAuth });
|
|
23
|
+
output(data, program.opts().format);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
fatal(err.message);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { loadCredentials } from "../auth.js";
|
|
2
|
+
import { callApi } from "../api.js";
|
|
3
|
+
import { output, fatal } from "../utils.js";
|
|
4
|
+
export function registerCommentThreadCommands(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("comment-threads")
|
|
7
|
+
.description("List comment threads for a video, channel, or by ID")
|
|
8
|
+
.option("--video-id <id>", "Video ID to list comments for")
|
|
9
|
+
.option("--channel-id <id>", "Channel ID to list comments for")
|
|
10
|
+
.option("--id <id>", "Comment thread ID(s), comma-separated")
|
|
11
|
+
.option("--part <parts>", "Parts to include", "snippet,replies")
|
|
12
|
+
.option("--max-results <n>", "Max results (1-100)", "20")
|
|
13
|
+
.option("--page-token <token>", "Pagination token")
|
|
14
|
+
.option("--order <order>", "Sort order (time, relevance)", "relevance")
|
|
15
|
+
.option("--search-terms <q>", "Filter by search terms")
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
try {
|
|
18
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
19
|
+
const params = {
|
|
20
|
+
part: opts.part,
|
|
21
|
+
maxResults: opts.maxResults,
|
|
22
|
+
order: opts.order,
|
|
23
|
+
};
|
|
24
|
+
if (opts.videoId)
|
|
25
|
+
params.videoId = opts.videoId;
|
|
26
|
+
else if (opts.channelId)
|
|
27
|
+
params.channelId = opts.channelId;
|
|
28
|
+
else if (opts.id)
|
|
29
|
+
params.id = opts.id;
|
|
30
|
+
else {
|
|
31
|
+
fatal("One of --video-id, --channel-id, or --id is required.");
|
|
32
|
+
}
|
|
33
|
+
if (opts.pageToken)
|
|
34
|
+
params.pageToken = opts.pageToken;
|
|
35
|
+
if (opts.searchTerms)
|
|
36
|
+
params.searchTerms = opts.searchTerms;
|
|
37
|
+
const data = await callApi("/commentThreads", { creds, params });
|
|
38
|
+
output(data, program.opts().format);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
fatal(err.message);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
program
|
|
45
|
+
.command("comment-threads-insert")
|
|
46
|
+
.description("Post a top-level comment on a video (OAuth required)")
|
|
47
|
+
.requiredOption("--video-id <id>", "Video ID to comment on")
|
|
48
|
+
.requiredOption("--text <text>", "Comment text")
|
|
49
|
+
.action(async (opts) => {
|
|
50
|
+
try {
|
|
51
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
52
|
+
const data = await callApi("/commentThreads", {
|
|
53
|
+
creds,
|
|
54
|
+
params: { part: "snippet" },
|
|
55
|
+
method: "POST",
|
|
56
|
+
body: {
|
|
57
|
+
snippet: {
|
|
58
|
+
videoId: opts.videoId,
|
|
59
|
+
topLevelComment: {
|
|
60
|
+
snippet: {
|
|
61
|
+
textOriginal: opts.text,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
output(data, program.opts().format);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
fatal(err.message);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { loadCredentials } from "../auth.js";
|
|
2
|
+
import { callApi } from "../api.js";
|
|
3
|
+
import { output, fatal } from "../utils.js";
|
|
4
|
+
export function registerCommentCommands(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("comments")
|
|
7
|
+
.description("List replies to a comment thread")
|
|
8
|
+
.requiredOption("--parent-id <id>", "Parent comment ID to list replies for")
|
|
9
|
+
.option("--part <parts>", "Parts to include", "snippet")
|
|
10
|
+
.option("--max-results <n>", "Max results (1-100)", "20")
|
|
11
|
+
.option("--page-token <token>", "Pagination token")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
try {
|
|
14
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
15
|
+
const params = {
|
|
16
|
+
part: opts.part,
|
|
17
|
+
parentId: opts.parentId,
|
|
18
|
+
maxResults: opts.maxResults,
|
|
19
|
+
};
|
|
20
|
+
if (opts.pageToken)
|
|
21
|
+
params.pageToken = opts.pageToken;
|
|
22
|
+
const data = await callApi("/comments", { creds, params });
|
|
23
|
+
output(data, program.opts().format);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
fatal(err.message);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
program
|
|
30
|
+
.command("comments-insert")
|
|
31
|
+
.description("Reply to a comment (OAuth required)")
|
|
32
|
+
.requiredOption("--parent-id <id>", "Parent comment ID to reply to")
|
|
33
|
+
.requiredOption("--text <text>", "Reply text")
|
|
34
|
+
.action(async (opts) => {
|
|
35
|
+
try {
|
|
36
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
37
|
+
const data = await callApi("/comments", {
|
|
38
|
+
creds,
|
|
39
|
+
params: { part: "snippet" },
|
|
40
|
+
method: "POST",
|
|
41
|
+
body: {
|
|
42
|
+
snippet: {
|
|
43
|
+
parentId: opts.parentId,
|
|
44
|
+
textOriginal: opts.text,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
output(data, program.opts().format);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
fatal(err.message);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
program
|
|
55
|
+
.command("comments-update")
|
|
56
|
+
.description("Update a comment (OAuth required)")
|
|
57
|
+
.requiredOption("--id <id>", "Comment ID")
|
|
58
|
+
.requiredOption("--text <text>", "Updated comment text")
|
|
59
|
+
.action(async (opts) => {
|
|
60
|
+
try {
|
|
61
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
62
|
+
const data = await callApi("/comments", {
|
|
63
|
+
creds,
|
|
64
|
+
params: { part: "snippet" },
|
|
65
|
+
method: "PUT",
|
|
66
|
+
body: {
|
|
67
|
+
id: opts.id,
|
|
68
|
+
snippet: {
|
|
69
|
+
textOriginal: opts.text,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
output(data, program.opts().format);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
fatal(err.message);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
program
|
|
80
|
+
.command("comments-delete")
|
|
81
|
+
.description("Delete a comment (OAuth required)")
|
|
82
|
+
.requiredOption("--id <id>", "Comment ID")
|
|
83
|
+
.action(async (opts) => {
|
|
84
|
+
try {
|
|
85
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
86
|
+
const data = await callApi("/comments", {
|
|
87
|
+
creds,
|
|
88
|
+
params: { id: opts.id },
|
|
89
|
+
method: "DELETE",
|
|
90
|
+
});
|
|
91
|
+
output(data, program.opts().format);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
fatal(err.message);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { loadCredentials } from "../auth.js";
|
|
2
|
+
import { callApi } from "../api.js";
|
|
3
|
+
import { output, fatal } from "../utils.js";
|
|
4
|
+
export function registerPlaylistItemCommands(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("playlist-items")
|
|
7
|
+
.description("List items in a playlist")
|
|
8
|
+
.requiredOption("--playlist-id <id>", "Playlist ID")
|
|
9
|
+
.option("--part <parts>", "Parts to include", "snippet,contentDetails")
|
|
10
|
+
.option("--max-results <n>", "Max results (1-50)", "25")
|
|
11
|
+
.option("--page-token <token>", "Pagination token")
|
|
12
|
+
.option("--video-id <id>", "Filter by specific video ID")
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
try {
|
|
15
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
16
|
+
const params = {
|
|
17
|
+
part: opts.part,
|
|
18
|
+
playlistId: opts.playlistId,
|
|
19
|
+
maxResults: opts.maxResults,
|
|
20
|
+
};
|
|
21
|
+
if (opts.pageToken)
|
|
22
|
+
params.pageToken = opts.pageToken;
|
|
23
|
+
if (opts.videoId)
|
|
24
|
+
params.videoId = opts.videoId;
|
|
25
|
+
const data = await callApi("/playlistItems", { creds, params });
|
|
26
|
+
output(data, program.opts().format);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
fatal(err.message);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
program
|
|
33
|
+
.command("playlist-items-insert")
|
|
34
|
+
.description("Add a video to a playlist (OAuth required)")
|
|
35
|
+
.requiredOption("--playlist-id <id>", "Playlist ID")
|
|
36
|
+
.requiredOption("--video-id <id>", "Video ID to add")
|
|
37
|
+
.option("--position <n>", "Position in the playlist (0-based)")
|
|
38
|
+
.action(async (opts) => {
|
|
39
|
+
try {
|
|
40
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
41
|
+
const snippet = {
|
|
42
|
+
playlistId: opts.playlistId,
|
|
43
|
+
resourceId: {
|
|
44
|
+
kind: "youtube#video",
|
|
45
|
+
videoId: opts.videoId,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
if (opts.position !== undefined) {
|
|
49
|
+
snippet.position = parseInt(opts.position, 10);
|
|
50
|
+
}
|
|
51
|
+
const data = await callApi("/playlistItems", {
|
|
52
|
+
creds,
|
|
53
|
+
params: { part: "snippet" },
|
|
54
|
+
method: "POST",
|
|
55
|
+
body: { snippet },
|
|
56
|
+
});
|
|
57
|
+
output(data, program.opts().format);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
fatal(err.message);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
program
|
|
64
|
+
.command("playlist-items-update")
|
|
65
|
+
.description("Update a playlist item (OAuth required)")
|
|
66
|
+
.requiredOption("--id <id>", "Playlist item ID")
|
|
67
|
+
.requiredOption("--playlist-id <id>", "Playlist ID")
|
|
68
|
+
.requiredOption("--video-id <id>", "Video ID")
|
|
69
|
+
.option("--position <n>", "New position in the playlist (0-based)")
|
|
70
|
+
.action(async (opts) => {
|
|
71
|
+
try {
|
|
72
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
73
|
+
const snippet = {
|
|
74
|
+
playlistId: opts.playlistId,
|
|
75
|
+
resourceId: {
|
|
76
|
+
kind: "youtube#video",
|
|
77
|
+
videoId: opts.videoId,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
if (opts.position !== undefined) {
|
|
81
|
+
snippet.position = parseInt(opts.position, 10);
|
|
82
|
+
}
|
|
83
|
+
const data = await callApi("/playlistItems", {
|
|
84
|
+
creds,
|
|
85
|
+
params: { part: "snippet" },
|
|
86
|
+
method: "PUT",
|
|
87
|
+
body: { id: opts.id, snippet },
|
|
88
|
+
});
|
|
89
|
+
output(data, program.opts().format);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
fatal(err.message);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
program
|
|
96
|
+
.command("playlist-items-delete")
|
|
97
|
+
.description("Remove an item from a playlist (OAuth required)")
|
|
98
|
+
.requiredOption("--id <id>", "Playlist item ID")
|
|
99
|
+
.action(async (opts) => {
|
|
100
|
+
try {
|
|
101
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
102
|
+
const data = await callApi("/playlistItems", {
|
|
103
|
+
creds,
|
|
104
|
+
params: { id: opts.id },
|
|
105
|
+
method: "DELETE",
|
|
106
|
+
});
|
|
107
|
+
output(data, program.opts().format);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
fatal(err.message);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { loadCredentials } from "../auth.js";
|
|
2
|
+
import { callApi } from "../api.js";
|
|
3
|
+
import { output, fatal } from "../utils.js";
|
|
4
|
+
export function registerPlaylistCommands(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("playlists")
|
|
7
|
+
.description("List playlists by ID, channel, or authenticated user")
|
|
8
|
+
.option("--id <id>", "Playlist ID(s), comma-separated")
|
|
9
|
+
.option("--channel-id <id>", "Channel ID to list playlists for")
|
|
10
|
+
.option("--mine", "List authenticated user's playlists (OAuth required)")
|
|
11
|
+
.option("--part <parts>", "Parts to include", "snippet,contentDetails")
|
|
12
|
+
.option("--max-results <n>", "Max results (1-50)", "25")
|
|
13
|
+
.option("--page-token <token>", "Pagination token")
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
try {
|
|
16
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
17
|
+
const params = {
|
|
18
|
+
part: opts.part,
|
|
19
|
+
maxResults: opts.maxResults,
|
|
20
|
+
};
|
|
21
|
+
const requireOAuth = !!opts.mine;
|
|
22
|
+
if (opts.id) {
|
|
23
|
+
params.id = opts.id;
|
|
24
|
+
}
|
|
25
|
+
else if (opts.channelId) {
|
|
26
|
+
params.channelId = opts.channelId;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
params.mine = "true";
|
|
30
|
+
}
|
|
31
|
+
if (opts.pageToken)
|
|
32
|
+
params.pageToken = opts.pageToken;
|
|
33
|
+
const data = await callApi("/playlists", { creds, params, requireOAuth });
|
|
34
|
+
output(data, program.opts().format);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
fatal(err.message);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
program
|
|
41
|
+
.command("playlists-insert")
|
|
42
|
+
.description("Create a new playlist (OAuth required)")
|
|
43
|
+
.requiredOption("--title <title>", "Playlist title")
|
|
44
|
+
.option("--description <desc>", "Playlist description")
|
|
45
|
+
.option("--privacy <status>", "Privacy status (public, private, unlisted)", "private")
|
|
46
|
+
.option("--default-language <lang>", "Default language (ISO 639-1)")
|
|
47
|
+
.action(async (opts) => {
|
|
48
|
+
try {
|
|
49
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
50
|
+
const body = {
|
|
51
|
+
snippet: {
|
|
52
|
+
title: opts.title,
|
|
53
|
+
...(opts.description && { description: opts.description }),
|
|
54
|
+
...(opts.defaultLanguage && { defaultLanguage: opts.defaultLanguage }),
|
|
55
|
+
},
|
|
56
|
+
status: {
|
|
57
|
+
privacyStatus: opts.privacy,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const data = await callApi("/playlists", {
|
|
61
|
+
creds,
|
|
62
|
+
params: { part: "snippet,status" },
|
|
63
|
+
method: "POST",
|
|
64
|
+
body,
|
|
65
|
+
});
|
|
66
|
+
output(data, program.opts().format);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
fatal(err.message);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
program
|
|
73
|
+
.command("playlists-update")
|
|
74
|
+
.description("Update a playlist (OAuth required)")
|
|
75
|
+
.requiredOption("--id <id>", "Playlist ID")
|
|
76
|
+
.requiredOption("--title <title>", "Playlist title")
|
|
77
|
+
.option("--description <desc>", "Playlist description")
|
|
78
|
+
.option("--privacy <status>", "Privacy status (public, private, unlisted)")
|
|
79
|
+
.option("--default-language <lang>", "Default language (ISO 639-1)")
|
|
80
|
+
.action(async (opts) => {
|
|
81
|
+
try {
|
|
82
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
83
|
+
const body = {
|
|
84
|
+
id: opts.id,
|
|
85
|
+
snippet: {
|
|
86
|
+
title: opts.title,
|
|
87
|
+
...(opts.description && { description: opts.description }),
|
|
88
|
+
...(opts.defaultLanguage && { defaultLanguage: opts.defaultLanguage }),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const parts = ["snippet"];
|
|
92
|
+
if (opts.privacy) {
|
|
93
|
+
body.status = { privacyStatus: opts.privacy };
|
|
94
|
+
parts.push("status");
|
|
95
|
+
}
|
|
96
|
+
const data = await callApi("/playlists", {
|
|
97
|
+
creds,
|
|
98
|
+
params: { part: parts.join(",") },
|
|
99
|
+
method: "PUT",
|
|
100
|
+
body,
|
|
101
|
+
});
|
|
102
|
+
output(data, program.opts().format);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
fatal(err.message);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
program
|
|
109
|
+
.command("playlists-delete")
|
|
110
|
+
.description("Delete a playlist (OAuth required)")
|
|
111
|
+
.requiredOption("--id <id>", "Playlist ID")
|
|
112
|
+
.action(async (opts) => {
|
|
113
|
+
try {
|
|
114
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
115
|
+
const data = await callApi("/playlists", {
|
|
116
|
+
creds,
|
|
117
|
+
params: { id: opts.id },
|
|
118
|
+
method: "DELETE",
|
|
119
|
+
});
|
|
120
|
+
output(data, program.opts().format);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
fatal(err.message);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { loadCredentials } from "../auth.js";
|
|
2
|
+
import { callApi } from "../api.js";
|
|
3
|
+
import { output, fatal } from "../utils.js";
|
|
4
|
+
export function registerSearchCommands(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("search")
|
|
7
|
+
.description("Search YouTube for videos, channels, and playlists")
|
|
8
|
+
.requiredOption("--q <query>", "Search query")
|
|
9
|
+
.option("--type <type>", "Resource type filter (video, channel, playlist)", "video,channel,playlist")
|
|
10
|
+
.option("--max-results <n>", "Max results (1-50)", "5")
|
|
11
|
+
.option("--order <order>", "Sort order (relevance, date, rating, title, videoCount, viewCount)", "relevance")
|
|
12
|
+
.option("--channel-id <id>", "Limit results to a specific channel")
|
|
13
|
+
.option("--page-token <token>", "Pagination token")
|
|
14
|
+
.option("--published-after <datetime>", "Filter results published after this date (RFC 3339)")
|
|
15
|
+
.option("--published-before <datetime>", "Filter results published before this date (RFC 3339)")
|
|
16
|
+
.option("--region-code <code>", "ISO 3166-1 alpha-2 country code")
|
|
17
|
+
.option("--relevance-language <lang>", "ISO 639-1 language code")
|
|
18
|
+
.option("--safe-search <level>", "Safe search filtering (none, moderate, strict)")
|
|
19
|
+
.option("--video-duration <duration>", "Video duration filter (any, short, medium, long)")
|
|
20
|
+
.option("--video-definition <def>", "Video definition filter (any, high, standard)")
|
|
21
|
+
.option("--video-type <type>", "Video type filter (any, episode, movie)")
|
|
22
|
+
.option("--event-type <type>", "Event type filter (completed, live, upcoming)")
|
|
23
|
+
.option("--topic-id <id>", "Freebase topic ID filter")
|
|
24
|
+
.option("--video-category-id <id>", "Video category ID filter")
|
|
25
|
+
.action(async (opts) => {
|
|
26
|
+
try {
|
|
27
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
28
|
+
const params = {
|
|
29
|
+
part: "snippet",
|
|
30
|
+
q: opts.q,
|
|
31
|
+
type: opts.type,
|
|
32
|
+
maxResults: opts.maxResults,
|
|
33
|
+
order: opts.order,
|
|
34
|
+
};
|
|
35
|
+
if (opts.channelId)
|
|
36
|
+
params.channelId = opts.channelId;
|
|
37
|
+
if (opts.pageToken)
|
|
38
|
+
params.pageToken = opts.pageToken;
|
|
39
|
+
if (opts.publishedAfter)
|
|
40
|
+
params.publishedAfter = opts.publishedAfter;
|
|
41
|
+
if (opts.publishedBefore)
|
|
42
|
+
params.publishedBefore = opts.publishedBefore;
|
|
43
|
+
if (opts.regionCode)
|
|
44
|
+
params.regionCode = opts.regionCode;
|
|
45
|
+
if (opts.relevanceLanguage)
|
|
46
|
+
params.relevanceLanguage = opts.relevanceLanguage;
|
|
47
|
+
if (opts.safeSearch)
|
|
48
|
+
params.safeSearch = opts.safeSearch;
|
|
49
|
+
if (opts.videoDuration)
|
|
50
|
+
params.videoDuration = opts.videoDuration;
|
|
51
|
+
if (opts.videoDefinition)
|
|
52
|
+
params.videoDefinition = opts.videoDefinition;
|
|
53
|
+
if (opts.videoType)
|
|
54
|
+
params.videoType = opts.videoType;
|
|
55
|
+
if (opts.eventType)
|
|
56
|
+
params.eventType = opts.eventType;
|
|
57
|
+
if (opts.topicId)
|
|
58
|
+
params.topicId = opts.topicId;
|
|
59
|
+
if (opts.videoCategoryId)
|
|
60
|
+
params.videoCategoryId = opts.videoCategoryId;
|
|
61
|
+
const data = await callApi("/search", { creds, params });
|
|
62
|
+
output(data, program.opts().format);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
fatal(err.message);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { loadCredentials } from "../auth.js";
|
|
2
|
+
import { callApi } from "../api.js";
|
|
3
|
+
import { output, fatal } from "../utils.js";
|
|
4
|
+
export function registerSubscriptionCommands(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("subscriptions")
|
|
7
|
+
.description("List subscriptions (OAuth required)")
|
|
8
|
+
.option("--channel-id <id>", "List subscriptions for a channel")
|
|
9
|
+
.option("--id <id>", "Subscription ID(s), comma-separated")
|
|
10
|
+
.option("--mine", "List authenticated user's subscriptions")
|
|
11
|
+
.option("--part <parts>", "Parts to include", "snippet,contentDetails")
|
|
12
|
+
.option("--max-results <n>", "Max results (1-50)", "25")
|
|
13
|
+
.option("--page-token <token>", "Pagination token")
|
|
14
|
+
.option("--order <order>", "Sort order (alphabetical, relevance, unread)")
|
|
15
|
+
.option("--for-channel-id <id>", "Filter by subscribed channel ID(s)")
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
try {
|
|
18
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
19
|
+
const params = {
|
|
20
|
+
part: opts.part,
|
|
21
|
+
maxResults: opts.maxResults,
|
|
22
|
+
};
|
|
23
|
+
const requireOAuth = !!opts.mine || (!opts.channelId && !opts.id);
|
|
24
|
+
if (opts.id) {
|
|
25
|
+
params.id = opts.id;
|
|
26
|
+
}
|
|
27
|
+
else if (opts.channelId) {
|
|
28
|
+
params.channelId = opts.channelId;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
params.mine = "true";
|
|
32
|
+
}
|
|
33
|
+
if (opts.pageToken)
|
|
34
|
+
params.pageToken = opts.pageToken;
|
|
35
|
+
if (opts.order)
|
|
36
|
+
params.order = opts.order;
|
|
37
|
+
if (opts.forChannelId)
|
|
38
|
+
params.forChannelId = opts.forChannelId;
|
|
39
|
+
const data = await callApi("/subscriptions", { creds, params, requireOAuth });
|
|
40
|
+
output(data, program.opts().format);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
fatal(err.message);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
program
|
|
47
|
+
.command("subscriptions-insert")
|
|
48
|
+
.description("Subscribe to a channel (OAuth required)")
|
|
49
|
+
.requiredOption("--channel-id <id>", "Channel ID to subscribe to")
|
|
50
|
+
.action(async (opts) => {
|
|
51
|
+
try {
|
|
52
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
53
|
+
const data = await callApi("/subscriptions", {
|
|
54
|
+
creds,
|
|
55
|
+
params: { part: "snippet" },
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: {
|
|
58
|
+
snippet: {
|
|
59
|
+
resourceId: {
|
|
60
|
+
kind: "youtube#channel",
|
|
61
|
+
channelId: opts.channelId,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
output(data, program.opts().format);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
fatal(err.message);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
program
|
|
73
|
+
.command("subscriptions-delete")
|
|
74
|
+
.description("Unsubscribe (OAuth required)")
|
|
75
|
+
.requiredOption("--id <id>", "Subscription ID")
|
|
76
|
+
.action(async (opts) => {
|
|
77
|
+
try {
|
|
78
|
+
const creds = loadCredentials(program.opts().credentials);
|
|
79
|
+
const data = await callApi("/subscriptions", {
|
|
80
|
+
creds,
|
|
81
|
+
params: { id: opts.id },
|
|
82
|
+
method: "DELETE",
|
|
83
|
+
});
|
|
84
|
+
output(data, program.opts().format);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
fatal(err.message);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|