@jackwener/opencli 1.8.0 → 1.8.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/README.md +8 -49
- package/README.zh-CN.md +8 -52
- package/cli-manifest.json +1796 -191
- package/clis/_atlassian/shared.js +577 -0
- package/clis/_atlassian/shared.test.js +170 -0
- package/clis/bilibili/comment.js +125 -0
- package/clis/bilibili/comment.test.js +153 -0
- package/clis/bilibili/comments.js +116 -21
- package/clis/bilibili/comments.test.js +77 -18
- package/clis/bilibili/subtitle.js +76 -31
- package/clis/bilibili/subtitle.test.js +156 -9
- package/clis/bilibili/utils.js +63 -5
- package/clis/bilibili/utils.test.js +45 -1
- package/clis/chess/analyze.js +35 -0
- package/clis/chess/analyze.test.js +79 -0
- package/clis/chess/game.js +114 -0
- package/clis/chess/game.test.js +178 -0
- package/clis/chess/games.js +67 -0
- package/clis/chess/games.test.js +164 -0
- package/clis/chess/stats.js +32 -0
- package/clis/chess/stats.test.js +79 -0
- package/clis/chess/utils.js +170 -0
- package/clis/chess/utils.test.js +230 -0
- package/clis/confluence/commands.test.js +195 -0
- package/clis/confluence/create.js +39 -0
- package/clis/confluence/page.js +23 -0
- package/clis/confluence/search.js +34 -0
- package/clis/confluence/shared.js +173 -0
- package/clis/confluence/update.js +38 -0
- package/clis/douyin/hashtag.js +84 -23
- package/clis/douyin/hashtag.test.js +113 -0
- package/clis/geogebra/add-circle.js +46 -0
- package/clis/geogebra/add-line.js +35 -0
- package/clis/geogebra/add-point.js +27 -0
- package/clis/geogebra/add-polygon.js +25 -0
- package/clis/geogebra/eval.js +35 -0
- package/clis/geogebra/geogebra.test.js +175 -0
- package/clis/geogebra/hexagon.js +62 -0
- package/clis/geogebra/info.js +72 -0
- package/clis/geogebra/list.js +35 -0
- package/clis/geogebra/triangle.js +60 -0
- package/clis/geogebra/utils.js +271 -0
- package/clis/jira/attachments.js +28 -0
- package/clis/jira/commands.test.js +287 -0
- package/clis/jira/comments.js +28 -0
- package/clis/jira/issue.js +28 -0
- package/clis/jira/links.js +28 -0
- package/clis/jira/search.js +47 -0
- package/clis/jira/shared.js +256 -0
- package/clis/linkedin/job-detail.js +167 -0
- package/clis/linkedin/job-detail.test.js +38 -0
- package/clis/linkedin/jobs-preferences.js +113 -0
- package/clis/linkedin/jobs-preferences.test.js +43 -0
- package/clis/linkedin/post-analytics.js +74 -0
- package/clis/linkedin/post-analytics.test.js +40 -0
- package/clis/linkedin/posts-core.js +241 -0
- package/clis/linkedin/posts.js +22 -0
- package/clis/linkedin/posts.test.js +40 -0
- package/clis/linkedin/profile-analytics.js +104 -0
- package/clis/linkedin/profile-analytics.test.js +67 -0
- package/clis/linkedin/profile-experience.js +671 -0
- package/clis/linkedin/profile-experience.test.js +152 -0
- package/clis/linkedin/profile-projects.js +311 -0
- package/clis/linkedin/profile-projects.test.js +111 -0
- package/clis/linkedin/profile-read.js +148 -0
- package/clis/linkedin/profile-read.test.js +77 -0
- package/clis/linkedin/services-read.js +213 -0
- package/clis/linkedin/services-read.test.js +105 -0
- package/clis/linkedin/shared.js +124 -0
- package/clis/linkedin/timeline.js +14 -7
- package/clis/notebooklm/add-source.js +269 -0
- package/clis/notebooklm/add-source.test.js +97 -0
- package/clis/notebooklm/create.js +76 -0
- package/clis/notebooklm/create.test.js +58 -0
- package/clis/notebooklm/generate-audio.js +91 -0
- package/clis/notebooklm/generate-audio.test.js +63 -0
- package/clis/notebooklm/generate-slides.js +106 -0
- package/clis/notebooklm/generate-slides.test.js +75 -0
- package/clis/notebooklm/open.test.js +10 -10
- package/clis/notebooklm/rpc.js +20 -6
- package/clis/notebooklm/rpc.test.js +27 -1
- package/clis/notebooklm/utils.js +100 -24
- package/clis/notebooklm/utils.test.js +60 -1
- package/clis/notebooklm/write-note.js +103 -0
- package/clis/notebooklm/write-note.test.js +70 -0
- package/clis/pixiv/detail.js +41 -34
- package/clis/pixiv/detail.test.js +93 -0
- package/clis/pixiv/user.js +36 -31
- package/clis/pixiv/user.test.js +100 -0
- package/clis/pixiv/utils.js +56 -7
- package/clis/suno/generate.js +5 -0
- package/clis/suno/generate.test.js +9 -0
- package/clis/suno/status.js +3 -2
- package/clis/suno/utils.js +33 -24
- package/clis/suno/utils.test.js +106 -0
- package/clis/twitter/followers.js +6 -2
- package/clis/twitter/followers.test.js +19 -1
- package/clis/twitter/following.js +14 -5
- package/clis/twitter/following.test.js +29 -0
- package/clis/twitter/likes.js +12 -4
- package/clis/twitter/likes.test.js +26 -1
- package/clis/twitter/list-add.js +1 -1
- package/clis/twitter/list-remove.js +1 -1
- package/clis/twitter/notifications.js +4 -4
- package/clis/twitter/post.js +62 -4
- package/clis/twitter/post.test.js +35 -3
- package/clis/twitter/profile.js +81 -28
- package/clis/twitter/profile.test.js +113 -2
- package/clis/twitter/quote.js +9 -4
- package/clis/twitter/reply.js +13 -10
- package/clis/twitter/reply.test.js +41 -0
- package/clis/twitter/search.js +1 -1
- package/clis/twitter/search.test.js +35 -0
- package/clis/twitter/shared.js +11 -0
- package/clis/twitter/shared.test.js +37 -1
- package/clis/twitter/utils.js +53 -16
- package/clis/upwork/detail.js +132 -0
- package/clis/upwork/feed.js +109 -0
- package/clis/upwork/search.js +115 -0
- package/clis/upwork/upwork.test.js +566 -0
- package/clis/upwork/utils.js +323 -0
- package/clis/weread/book-search.js +438 -0
- package/clis/weread/book-search.test.js +242 -0
- package/clis/weread/search-regression.test.js +80 -0
- package/clis/weread/search.js +17 -2
- package/clis/xiaohongshu/creator-note-detail.js +165 -28
- package/clis/xiaohongshu/creator-note-detail.test.js +186 -37
- package/clis/xiaohongshu/creator-notes.js +251 -2
- package/clis/xiaohongshu/creator-notes.test.js +79 -2
- package/clis/xiaohongshu/download.js +97 -39
- package/clis/xiaohongshu/download.test.js +201 -0
- package/clis/zhihu/answer-comments.js +2 -21
- package/clis/zhihu/answer-detail.js +2 -31
- package/clis/zhihu/collection.js +2 -14
- package/clis/zhihu/collection.test.js +4 -3
- package/clis/zhihu/question.js +1 -9
- package/clis/zhihu/question.test.js +2 -2
- package/clis/zhihu/search.js +1 -12
- package/clis/zhihu/search.test.js +2 -2
- package/clis/zhihu/text.js +29 -0
- package/clis/zhihu/text.test.js +24 -0
- package/dist/src/browser/network-cache.js +13 -1
- package/dist/src/browser/network-cache.test.js +17 -0
- package/dist/src/download/index.js +13 -1
- package/dist/src/download/index.test.js +23 -1
- package/dist/src/download/media-download.test.js +3 -1
- package/dist/src/download/progress.js +2 -2
- package/dist/src/download/progress.test.js +12 -1
- package/dist/src/output.js +11 -1
- package/dist/src/output.test.js +6 -0
- package/dist/src/registry.js +1 -0
- package/dist/src/registry.test.js +11 -0
- package/package.json +1 -1
package/clis/notebooklm/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AuthRequiredError, CliError } from '@jackwener/opencli/errors';
|
|
1
|
+
import { ArgumentError, AuthRequiredError, CliError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
2
2
|
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_HOME_URL, } from './shared.js';
|
|
3
|
-
import { callNotebooklmRpc, getNotebooklmPageAuth, } from './rpc.js';
|
|
3
|
+
import { callNotebooklmRpc, getNotebooklmPageAuth, unwrapNotebooklmEvaluateResult, } from './rpc.js';
|
|
4
4
|
export { buildNotebooklmRpcBody, extractNotebooklmRpcResult, fetchNotebooklmInPage, getNotebooklmPageAuth, parseNotebooklmChunkedResponse, stripNotebooklmAntiXssi, } from './rpc.js';
|
|
5
5
|
const NOTEBOOKLM_LIST_RPC_ID = 'wXbhsf';
|
|
6
6
|
const NOTEBOOKLM_NOTEBOOK_DETAIL_RPC_ID = 'rLM1Ne';
|
|
@@ -13,28 +13,61 @@ function unwrapNotebooklmSingletonResult(result) {
|
|
|
13
13
|
}
|
|
14
14
|
return current;
|
|
15
15
|
}
|
|
16
|
+
export function isPlainObject(value) {
|
|
17
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
}
|
|
16
19
|
export function parseNotebooklmIdFromUrl(url) {
|
|
17
20
|
const match = url.match(/\/notebook\/([^/?#]+)/);
|
|
18
21
|
return match?.[1] ?? '';
|
|
19
22
|
}
|
|
23
|
+
const NOTEBOOK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
+
function ensureNotebookUuid(candidate) {
|
|
25
|
+
if (!NOTEBOOK_UUID_RE.test(candidate)) {
|
|
26
|
+
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', `NotebookLM notebook id "${candidate}" is not a valid UUID`, 'Pass a notebook id from `opencli notebooklm list` or a full notebook URL like https://notebooklm.google.com/notebook/<uuid>.');
|
|
27
|
+
}
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
20
30
|
export function parseNotebooklmNotebookTarget(value) {
|
|
21
31
|
const normalized = value.trim();
|
|
22
32
|
if (!normalized) {
|
|
23
33
|
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook id is required', 'Pass a notebook id from `opencli notebooklm list` or a full notebook URL.');
|
|
24
34
|
}
|
|
25
35
|
if (/^https?:\/\//i.test(normalized)) {
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = new URL(normalized);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook URL is invalid', 'Pass a full NotebookLM notebook URL like https://notebooklm.google.com/notebook/<uuid>.');
|
|
42
|
+
}
|
|
43
|
+
if (parsed.protocol !== 'https:' || parsed.hostname !== NOTEBOOKLM_DOMAIN || parsed.username || parsed.password || parsed.port) {
|
|
44
|
+
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook URL must be a canonical https://notebooklm.google.com URL', 'Pass a notebook id from `opencli notebooklm list` or a full NotebookLM notebook URL.');
|
|
45
|
+
}
|
|
26
46
|
const notebookId = parseNotebooklmIdFromUrl(normalized);
|
|
27
|
-
if (notebookId)
|
|
28
|
-
|
|
29
|
-
|
|
47
|
+
if (!notebookId) {
|
|
48
|
+
throw new CliError('NOTEBOOKLM_INVALID_NOTEBOOK', 'NotebookLM notebook URL is invalid', 'Pass a full NotebookLM notebook URL like https://notebooklm.google.com/notebook/<uuid>.');
|
|
49
|
+
}
|
|
50
|
+
return ensureNotebookUuid(notebookId);
|
|
30
51
|
}
|
|
31
52
|
const pathMatch = normalized.match(/(?:^|\/)notebook\/([^/?#]+)/);
|
|
32
53
|
if (pathMatch?.[1])
|
|
33
|
-
return pathMatch[1];
|
|
34
|
-
return normalized;
|
|
54
|
+
return ensureNotebookUuid(pathMatch[1]);
|
|
55
|
+
return ensureNotebookUuid(normalized);
|
|
56
|
+
}
|
|
57
|
+
export function getNotebooklmAuthuser() {
|
|
58
|
+
const v = process.env.OPENCLI_NOTEBOOKLM_AUTHUSER;
|
|
59
|
+
return typeof v === 'string' && /^\d+$/.test(v) ? v : '';
|
|
60
|
+
}
|
|
61
|
+
export function requireNotebooklmExecute(value, action) {
|
|
62
|
+
if (value !== true) {
|
|
63
|
+
throw new ArgumentError(`Refusing to ${action}: pass --execute to perform this NotebookLM write`);
|
|
64
|
+
}
|
|
35
65
|
}
|
|
36
66
|
export function buildNotebooklmNotebookUrl(notebookId) {
|
|
37
|
-
|
|
67
|
+
const u = new URL(`/notebook/${encodeURIComponent(notebookId)}`, NOTEBOOKLM_HOME_URL);
|
|
68
|
+
const authuser = getNotebooklmAuthuser();
|
|
69
|
+
if (authuser) u.searchParams.set('authuser', authuser);
|
|
70
|
+
return u.toString();
|
|
38
71
|
}
|
|
39
72
|
export function classifyNotebooklmPage(url) {
|
|
40
73
|
try {
|
|
@@ -401,6 +434,42 @@ export async function getNotebooklmDetailViaRpc(page) {
|
|
|
401
434
|
const rpc = await callNotebooklmRpc(page, NOTEBOOKLM_NOTEBOOK_DETAIL_RPC_ID, [state.notebookId, null, [2], null, 0]);
|
|
402
435
|
return parseNotebooklmNotebookDetailResult(rpc.result);
|
|
403
436
|
}
|
|
437
|
+
export async function getNotebooklmNotebookDetailById(page, notebookId) {
|
|
438
|
+
const rpc = await callNotebooklmRpc(page, NOTEBOOKLM_NOTEBOOK_DETAIL_RPC_ID, [notebookId, null, [2], null, 0]);
|
|
439
|
+
return { detail: parseNotebooklmNotebookDetailResult(rpc.result), sources: parseNotebooklmSourceListResult(rpc.result) };
|
|
440
|
+
}
|
|
441
|
+
export async function verifyNotebooklmNotebookExists(page, notebookId, action) {
|
|
442
|
+
try {
|
|
443
|
+
const { detail } = await getNotebooklmNotebookDetailById(page, notebookId);
|
|
444
|
+
if (!detail || detail.id !== notebookId) {
|
|
445
|
+
throw new CommandExecutionError(`NotebookLM ${action} succeeded but the notebook ${notebookId} was not found in the post-write verification`);
|
|
446
|
+
}
|
|
447
|
+
return detail;
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
if (error instanceof AuthRequiredError || error instanceof CommandExecutionError)
|
|
451
|
+
throw error;
|
|
452
|
+
throw new CommandExecutionError(`NotebookLM ${action} post-write verification failed: ${error?.message || error}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
export async function verifyNotebooklmSourceAdded(page, notebookId, sourceId, action) {
|
|
456
|
+
try {
|
|
457
|
+
const { detail, sources } = await getNotebooklmNotebookDetailById(page, notebookId);
|
|
458
|
+
if (!detail || detail.id !== notebookId) {
|
|
459
|
+
throw new CommandExecutionError(`NotebookLM ${action} succeeded but the notebook ${notebookId} was not found in the post-write verification`);
|
|
460
|
+
}
|
|
461
|
+
const matched = sources.find((s) => s.id === sourceId);
|
|
462
|
+
if (!matched) {
|
|
463
|
+
throw new CommandExecutionError(`NotebookLM ${action} succeeded but source ${sourceId} did not appear in the notebook's source list`);
|
|
464
|
+
}
|
|
465
|
+
return matched;
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
if (error instanceof AuthRequiredError || error instanceof CommandExecutionError)
|
|
469
|
+
throw error;
|
|
470
|
+
throw new CommandExecutionError(`NotebookLM ${action} post-write verification failed: ${error?.message || error}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
404
473
|
export async function listNotebooklmSourcesViaRpc(page) {
|
|
405
474
|
const state = await getNotebooklmPageState(page);
|
|
406
475
|
if (state.kind !== 'notebook' || !state.notebookId)
|
|
@@ -434,7 +503,7 @@ export async function listNotebooklmNotesFromPage(page) {
|
|
|
434
503
|
const state = await getNotebooklmPageState(page);
|
|
435
504
|
if (state.kind !== 'notebook' || !state.notebookId)
|
|
436
505
|
return [];
|
|
437
|
-
const raw = await page.evaluate(`(() => {
|
|
506
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
438
507
|
return Array.from(document.querySelectorAll('artifact-library-note')).map((node) => {
|
|
439
508
|
const titleNode = node.querySelector('.artifact-title');
|
|
440
509
|
return {
|
|
@@ -442,7 +511,7 @@ export async function listNotebooklmNotesFromPage(page) {
|
|
|
442
511
|
text: (node.innerText || node.textContent || '').replace(/\\s+/g, ' ').trim(),
|
|
443
512
|
};
|
|
444
513
|
});
|
|
445
|
-
})()`);
|
|
514
|
+
})()`));
|
|
446
515
|
if (!Array.isArray(raw) || raw.length === 0)
|
|
447
516
|
return [];
|
|
448
517
|
return parseNotebooklmNoteListRawRows(raw, state.notebookId, state.url || `https://${NOTEBOOKLM_DOMAIN}/notebook/${state.notebookId}`);
|
|
@@ -451,13 +520,13 @@ export async function readNotebooklmSummaryFromPage(page) {
|
|
|
451
520
|
const state = await getNotebooklmPageState(page);
|
|
452
521
|
if (state.kind !== 'notebook' || !state.notebookId)
|
|
453
522
|
return null;
|
|
454
|
-
const raw = await page.evaluate(`(() => {
|
|
523
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
455
524
|
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
456
525
|
const title = normalize(document.querySelector('.notebook-title, h1, [data-testid="notebook-title"]')?.textContent || document.title || '');
|
|
457
526
|
const summaryNode = document.querySelector('.notebook-summary, .summary-content, [class*="summary"]');
|
|
458
527
|
const summary = normalize(summaryNode?.textContent || '');
|
|
459
528
|
return { title, summary };
|
|
460
|
-
})()`);
|
|
529
|
+
})()`));
|
|
461
530
|
return parseNotebooklmSummaryRawRow(raw, state.notebookId, state.url || `https://${NOTEBOOKLM_DOMAIN}/notebook/${state.notebookId}`);
|
|
462
531
|
}
|
|
463
532
|
export async function getNotebooklmSummaryViaRpc(page) {
|
|
@@ -499,7 +568,7 @@ export async function readNotebooklmVisibleNoteFromPage(page) {
|
|
|
499
568
|
const state = await getNotebooklmPageState(page);
|
|
500
569
|
if (state.kind !== 'notebook' || !state.notebookId)
|
|
501
570
|
return null;
|
|
502
|
-
const raw = await page.evaluate(`(() => {
|
|
571
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
503
572
|
const normalizeText = (value) => (value || '').replace(/\\u00a0/g, ' ').replace(/\\r\\n/g, '\\n').trim();
|
|
504
573
|
const titleNode = document.querySelector('.note-header__editable-title');
|
|
505
574
|
const title = titleNode instanceof HTMLInputElement || titleNode instanceof HTMLTextAreaElement
|
|
@@ -516,7 +585,7 @@ export async function readNotebooklmVisibleNoteFromPage(page) {
|
|
|
516
585
|
title: normalizeText(title),
|
|
517
586
|
content: normalizeText(content),
|
|
518
587
|
};
|
|
519
|
-
})()`);
|
|
588
|
+
})()`));
|
|
520
589
|
return parseNotebooklmVisibleNoteRawRow(raw, state.notebookId, state.url || `https://${NOTEBOOKLM_DOMAIN}/notebook/${state.notebookId}`);
|
|
521
590
|
}
|
|
522
591
|
export async function ensureNotebooklmHome(page) {
|
|
@@ -526,11 +595,18 @@ export async function ensureNotebooklmHome(page) {
|
|
|
526
595
|
const currentKind = currentUrl ? classifyNotebooklmPage(currentUrl) : 'unknown';
|
|
527
596
|
if (currentKind === 'home')
|
|
528
597
|
return;
|
|
529
|
-
|
|
530
|
-
|
|
598
|
+
const authuser = getNotebooklmAuthuser();
|
|
599
|
+
const target = authuser ? `${NOTEBOOKLM_HOME_URL}?authuser=${encodeURIComponent(authuser)}` : NOTEBOOKLM_HOME_URL;
|
|
600
|
+
try {
|
|
601
|
+
await page.goto(target);
|
|
602
|
+
await page.wait(2);
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
throw new CommandExecutionError(`Failed to open NotebookLM home: ${error?.message || error}`);
|
|
606
|
+
}
|
|
531
607
|
}
|
|
532
608
|
export async function getNotebooklmPageState(page) {
|
|
533
|
-
const raw = await page.evaluate(`(() => {
|
|
609
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
534
610
|
const url = window.location.href;
|
|
535
611
|
const title = document.title || '';
|
|
536
612
|
const hostname = window.location.hostname || '';
|
|
@@ -557,7 +633,7 @@ export async function getNotebooklmPageState(page) {
|
|
|
557
633
|
.reduce((count, href, index, list) => list.indexOf(href) === index ? count + 1 : count, 0);
|
|
558
634
|
|
|
559
635
|
return { url, title, hostname, kind, notebookId, loginRequired, notebookCount, path };
|
|
560
|
-
})()`);
|
|
636
|
+
})()`));
|
|
561
637
|
const state = {
|
|
562
638
|
url: String(raw?.url ?? ''),
|
|
563
639
|
title: normalizeNotebooklmTitle(raw?.title, 'NotebookLM'),
|
|
@@ -582,7 +658,7 @@ export async function getNotebooklmPageState(page) {
|
|
|
582
658
|
return state;
|
|
583
659
|
}
|
|
584
660
|
export async function readCurrentNotebooklm(page) {
|
|
585
|
-
const raw = await page.evaluate(`(() => {
|
|
661
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
586
662
|
const url = window.location.href;
|
|
587
663
|
const match = url.match(/\\/notebook\\/([^/?#]+)/);
|
|
588
664
|
if (!match) return null;
|
|
@@ -595,7 +671,7 @@ export async function readCurrentNotebooklm(page) {
|
|
|
595
671
|
url,
|
|
596
672
|
source: 'current-page',
|
|
597
673
|
};
|
|
598
|
-
})()`);
|
|
674
|
+
})()`));
|
|
599
675
|
if (!raw)
|
|
600
676
|
return null;
|
|
601
677
|
return {
|
|
@@ -608,7 +684,7 @@ export async function readCurrentNotebooklm(page) {
|
|
|
608
684
|
};
|
|
609
685
|
}
|
|
610
686
|
export async function listNotebooklmLinks(page) {
|
|
611
|
-
const raw = await page.evaluate(`(() => {
|
|
687
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
612
688
|
const rows = [];
|
|
613
689
|
const seen = new Set();
|
|
614
690
|
|
|
@@ -656,7 +732,7 @@ export async function listNotebooklmLinks(page) {
|
|
|
656
732
|
}
|
|
657
733
|
|
|
658
734
|
return rows;
|
|
659
|
-
})()`);
|
|
735
|
+
})()`));
|
|
660
736
|
if (!Array.isArray(raw))
|
|
661
737
|
return [];
|
|
662
738
|
return raw
|
|
@@ -671,7 +747,7 @@ export async function listNotebooklmLinks(page) {
|
|
|
671
747
|
.filter((row) => row.id && row.url);
|
|
672
748
|
}
|
|
673
749
|
export async function listNotebooklmSourcesFromPage(page) {
|
|
674
|
-
const raw = await page.evaluate(`(() => {
|
|
750
|
+
const raw = unwrapNotebooklmEvaluateResult(await page.evaluate(`(() => {
|
|
675
751
|
const notebookMatch = window.location.href.match(/\\/notebook\\/([^/?#]+)/);
|
|
676
752
|
const notebookId = notebookMatch ? notebookMatch[1] : '';
|
|
677
753
|
if (!notebookId) return [];
|
|
@@ -721,7 +797,7 @@ export async function listNotebooklmSourcesFromPage(page) {
|
|
|
721
797
|
});
|
|
722
798
|
}
|
|
723
799
|
return rows;
|
|
724
|
-
})()`);
|
|
800
|
+
})()`));
|
|
725
801
|
if (!Array.isArray(raw))
|
|
726
802
|
return [];
|
|
727
803
|
return raw.filter((row) => row.id && row.title);
|
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { buildNotebooklmRpcBody, classifyNotebooklmPage, extractNotebooklmHistoryPreview, extractNotebooklmRpcResult, getNotebooklmPageState, normalizeNotebooklmTitle, parseNotebooklmHistoryThreadIdsResult, parseNotebooklmIdFromUrl, parseNotebooklmListResult, parseNotebooklmNoteListRawRows, parseNotebooklmNotebookDetailResult, parseNotebooklmSourceFulltextResult, parseNotebooklmSourceGuideResult, parseNotebooklmSourceListResult, } from './utils.js';
|
|
2
|
+
import { buildNotebooklmRpcBody, classifyNotebooklmPage, extractNotebooklmHistoryPreview, extractNotebooklmRpcResult, getNotebooklmPageState, isPlainObject, normalizeNotebooklmTitle, parseNotebooklmHistoryThreadIdsResult, parseNotebooklmIdFromUrl, parseNotebooklmListResult, parseNotebooklmNoteListRawRows, parseNotebooklmNotebookDetailResult, parseNotebooklmNotebookTarget, parseNotebooklmSourceFulltextResult, parseNotebooklmSourceGuideResult, parseNotebooklmSourceListResult, } from './utils.js';
|
|
3
|
+
import { CliError } from '@jackwener/opencli/errors';
|
|
3
4
|
describe('notebooklm utils', () => {
|
|
5
|
+
it('isPlainObject distinguishes objects from arrays / null / primitives', () => {
|
|
6
|
+
expect(isPlainObject({})).toBe(true);
|
|
7
|
+
expect(isPlainObject({ a: 1 })).toBe(true);
|
|
8
|
+
expect(isPlainObject([])).toBe(false);
|
|
9
|
+
expect(isPlainObject(null)).toBe(false);
|
|
10
|
+
expect(isPlainObject('x')).toBe(false);
|
|
11
|
+
expect(isPlainObject(0)).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
it('parseNotebooklmNotebookTarget accepts canonical uuid input', () => {
|
|
14
|
+
const id = '17e2b882-aaaa-bbbb-cccc-abcdef012345';
|
|
15
|
+
expect(parseNotebooklmNotebookTarget(id)).toBe(id);
|
|
16
|
+
});
|
|
17
|
+
it('parseNotebooklmNotebookTarget accepts a notebook url with uuid', () => {
|
|
18
|
+
const id = '17e2b882-aaaa-bbbb-cccc-abcdef012345';
|
|
19
|
+
expect(parseNotebooklmNotebookTarget(`https://notebooklm.google.com/notebook/${id}?pli=1`)).toBe(id);
|
|
20
|
+
});
|
|
21
|
+
it('parseNotebooklmNotebookTarget rejects non-uuid bare ids', () => {
|
|
22
|
+
expect(() => parseNotebooklmNotebookTarget('nb-demo')).toThrow(CliError);
|
|
23
|
+
});
|
|
24
|
+
it('parseNotebooklmNotebookTarget rejects malformed notebook urls', () => {
|
|
25
|
+
expect(() => parseNotebooklmNotebookTarget('https://notebooklm.google.com/notebook/not-a-uuid')).toThrow(CliError);
|
|
26
|
+
});
|
|
27
|
+
it('parseNotebooklmNotebookTarget rejects off-domain or non-canonical notebook urls', () => {
|
|
28
|
+
const id = '17e2b882-aaaa-bbbb-cccc-abcdef012345';
|
|
29
|
+
expect(() => parseNotebooklmNotebookTarget(`https://evil.test/notebook/${id}`)).toThrow(CliError);
|
|
30
|
+
expect(() => parseNotebooklmNotebookTarget(`http://notebooklm.google.com/notebook/${id}`)).toThrow(CliError);
|
|
31
|
+
expect(() => parseNotebooklmNotebookTarget(`https://notebooklm.google.com:444/notebook/${id}`)).toThrow(CliError);
|
|
32
|
+
expect(() => parseNotebooklmNotebookTarget(`https://user:notsecret@notebooklm.google.com/notebook/${id}`)).toThrow(CliError);
|
|
33
|
+
});
|
|
34
|
+
it('parseNotebooklmNotebookTarget rejects empty input', () => {
|
|
35
|
+
expect(() => parseNotebooklmNotebookTarget('')).toThrow(CliError);
|
|
36
|
+
expect(() => parseNotebooklmNotebookTarget(' ')).toThrow(CliError);
|
|
37
|
+
});
|
|
4
38
|
it('parses notebook id from a notebook url', () => {
|
|
5
39
|
expect(parseNotebooklmIdFromUrl('https://notebooklm.google.com/notebook/abc-123')).toBe('abc-123');
|
|
6
40
|
});
|
|
@@ -387,4 +421,29 @@ describe('notebooklm utils', () => {
|
|
|
387
421
|
notebookCount: 0,
|
|
388
422
|
});
|
|
389
423
|
});
|
|
424
|
+
it('reads page state through Browser Bridge evaluate envelopes', async () => {
|
|
425
|
+
const page = {
|
|
426
|
+
evaluate: async () => ({
|
|
427
|
+
session: 'site:notebooklm:abc',
|
|
428
|
+
data: {
|
|
429
|
+
url: 'https://notebooklm.google.com/notebook/nb-demo',
|
|
430
|
+
title: 'Demo Notebook - NotebookLM',
|
|
431
|
+
hostname: 'notebooklm.google.com',
|
|
432
|
+
kind: 'notebook',
|
|
433
|
+
notebookId: 'nb-demo',
|
|
434
|
+
loginRequired: false,
|
|
435
|
+
notebookCount: 0,
|
|
436
|
+
},
|
|
437
|
+
}),
|
|
438
|
+
};
|
|
439
|
+
await expect(getNotebooklmPageState(page)).resolves.toEqual({
|
|
440
|
+
url: 'https://notebooklm.google.com/notebook/nb-demo',
|
|
441
|
+
title: 'Demo Notebook - NotebookLM',
|
|
442
|
+
hostname: 'notebooklm.google.com',
|
|
443
|
+
kind: 'notebook',
|
|
444
|
+
notebookId: 'nb-demo',
|
|
445
|
+
loginRequired: false,
|
|
446
|
+
notebookCount: 0,
|
|
447
|
+
});
|
|
448
|
+
});
|
|
390
449
|
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { NOTEBOOKLM_DOMAIN, NOTEBOOKLM_SITE } from './shared.js';
|
|
4
|
+
import { callNotebooklmRpc } from './rpc.js';
|
|
5
|
+
import { buildNotebooklmNotebookUrl, ensureNotebooklmHome, parseNotebooklmNotebookTarget, requireNotebooklmExecute, requireNotebooklmSession } from './utils.js';
|
|
6
|
+
|
|
7
|
+
const NOTEBOOKLM_CREATE_NOTE_RPC_ID = 'CYK0Xb';
|
|
8
|
+
const NOTEBOOKLM_MUTATE_NOTE_RPC_ID = 'cYAfTb';
|
|
9
|
+
const MAX_TITLE_LEN = 200;
|
|
10
|
+
const MAX_CONTENT_LEN = 1_000_000;
|
|
11
|
+
const NOTE_UUID_RE = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
12
|
+
|
|
13
|
+
export function parseNoteTitle(value) {
|
|
14
|
+
const title = String(value ?? '').trim();
|
|
15
|
+
if (!title) throw new ArgumentError('--title is required');
|
|
16
|
+
if (title.length > MAX_TITLE_LEN) {
|
|
17
|
+
throw new ArgumentError(`--title must be at most ${MAX_TITLE_LEN} characters (got ${title.length})`);
|
|
18
|
+
}
|
|
19
|
+
return title;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseNoteContent(value) {
|
|
23
|
+
const content = String(value ?? '');
|
|
24
|
+
if (!content) throw new ArgumentError('--content is required');
|
|
25
|
+
if (content.length > MAX_CONTENT_LEN) {
|
|
26
|
+
throw new ArgumentError(`--content exceeds ${MAX_CONTENT_LEN} characters; split into smaller notes.`);
|
|
27
|
+
}
|
|
28
|
+
return content;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildCreateNoteShellArgs(projectId) {
|
|
32
|
+
return [projectId, '', [1], null, 'New Note', null, [2]];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildMutateNoteArgs(projectId, noteId, content, title) {
|
|
36
|
+
return [projectId, noteId, [[[content, title, [], 0]]], [2]];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toExcludedUuidSet(excludedIds) {
|
|
40
|
+
return new Set(excludedIds.map((id) => String(id ?? '').toLowerCase()).filter(Boolean));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseNoteIdFromResult(result, excludedIds = []) {
|
|
44
|
+
const excluded = toExcludedUuidSet(excludedIds);
|
|
45
|
+
if (typeof result === 'string') return NOTE_UUID_RE.test(result) && !excluded.has(result.toLowerCase()) ? result : '';
|
|
46
|
+
const stack = [result];
|
|
47
|
+
while (stack.length) {
|
|
48
|
+
const node = stack.shift();
|
|
49
|
+
if (typeof node === 'string') {
|
|
50
|
+
if (NOTE_UUID_RE.test(node) && !excluded.has(node.toLowerCase())) return node;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(node)) for (const child of node) stack.push(child);
|
|
54
|
+
else if (node && typeof node === 'object') for (const v of Object.values(node)) stack.push(v);
|
|
55
|
+
}
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
cli({
|
|
60
|
+
site: NOTEBOOKLM_SITE,
|
|
61
|
+
name: 'write-note',
|
|
62
|
+
access: 'write',
|
|
63
|
+
description: 'Create a Studio note in an existing NotebookLM notebook with the given title and Markdown content',
|
|
64
|
+
domain: NOTEBOOKLM_DOMAIN,
|
|
65
|
+
strategy: Strategy.COOKIE,
|
|
66
|
+
browser: true,
|
|
67
|
+
navigateBefore: false,
|
|
68
|
+
args: [
|
|
69
|
+
{ name: 'notebook', positional: true, required: true, help: 'Notebook id from `notebooklm list` or full notebook URL' },
|
|
70
|
+
{ name: 'title', required: true, help: 'Note title (1-200 chars)' },
|
|
71
|
+
{ name: 'content', required: true, help: 'Note body as Markdown' },
|
|
72
|
+
{ name: 'execute', type: 'boolean', help: 'Actually create the remote NotebookLM note' },
|
|
73
|
+
],
|
|
74
|
+
columns: ['notebook_id', 'note_id', 'title', 'notebook_url'],
|
|
75
|
+
func: async (page, kwargs) => {
|
|
76
|
+
const notebookId = parseNotebooklmNotebookTarget(String(kwargs.notebook ?? ''));
|
|
77
|
+
const title = parseNoteTitle(kwargs.title);
|
|
78
|
+
const content = parseNoteContent(kwargs.content);
|
|
79
|
+
requireNotebooklmExecute(kwargs.execute, 'create a NotebookLM note');
|
|
80
|
+
await ensureNotebooklmHome(page);
|
|
81
|
+
await requireNotebooklmSession(page);
|
|
82
|
+
const shellRpc = await callNotebooklmRpc(page, NOTEBOOKLM_CREATE_NOTE_RPC_ID, buildCreateNoteShellArgs(notebookId));
|
|
83
|
+
const noteId = parseNoteIdFromResult(shellRpc.result, [notebookId]);
|
|
84
|
+
if (!noteId) {
|
|
85
|
+
throw new CommandExecutionError('NotebookLM CreateNote RPC returned no note id');
|
|
86
|
+
}
|
|
87
|
+
await callNotebooklmRpc(page, NOTEBOOKLM_MUTATE_NOTE_RPC_ID, buildMutateNoteArgs(notebookId, noteId, content, title));
|
|
88
|
+
return [{
|
|
89
|
+
notebook_id: notebookId,
|
|
90
|
+
note_id: noteId,
|
|
91
|
+
title,
|
|
92
|
+
notebook_url: buildNotebooklmNotebookUrl(notebookId),
|
|
93
|
+
}];
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export const __test__ = {
|
|
98
|
+
parseNoteTitle,
|
|
99
|
+
parseNoteContent,
|
|
100
|
+
buildCreateNoteShellArgs,
|
|
101
|
+
buildMutateNoteArgs,
|
|
102
|
+
parseNoteIdFromResult,
|
|
103
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
4
|
+
import { __test__ } from './write-note.js';
|
|
5
|
+
|
|
6
|
+
const { parseNoteTitle, parseNoteContent, buildCreateNoteShellArgs, buildMutateNoteArgs, parseNoteIdFromResult } = __test__;
|
|
7
|
+
|
|
8
|
+
describe('notebooklm write-note', () => {
|
|
9
|
+
it('parseNoteTitle accepts 1-200 char titles', () => {
|
|
10
|
+
expect(parseNoteTitle('Note A')).toBe('Note A');
|
|
11
|
+
expect(parseNoteTitle(' spaced ')).toBe('spaced');
|
|
12
|
+
expect(parseNoteTitle('x'.repeat(200))).toHaveLength(200);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('parseNoteTitle rejects empty / too-long titles', () => {
|
|
16
|
+
expect(() => parseNoteTitle('')).toThrow(ArgumentError);
|
|
17
|
+
expect(() => parseNoteTitle(' ')).toThrow(ArgumentError);
|
|
18
|
+
expect(() => parseNoteTitle('x'.repeat(201))).toThrow(ArgumentError);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('parseNoteContent accepts non-empty content', () => {
|
|
22
|
+
expect(parseNoteContent('# heading\n\nbody')).toBe('# heading\n\nbody');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('parseNoteContent rejects empty content', () => {
|
|
26
|
+
expect(() => parseNoteContent('')).toThrow(ArgumentError);
|
|
27
|
+
expect(() => parseNoteContent(undefined)).toThrow(ArgumentError);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('buildCreateNoteShellArgs matches the HAR-verified wire format', () => {
|
|
31
|
+
expect(buildCreateNoteShellArgs('nb-123')).toEqual([
|
|
32
|
+
'nb-123', '', [1], null, 'New Note', null, [2],
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('buildMutateNoteArgs puts content before title in the inner tuple', () => {
|
|
37
|
+
expect(buildMutateNoteArgs('nb-123', 'note-7', 'body content', 'title-x')).toEqual([
|
|
38
|
+
'nb-123', 'note-7', [[['body content', 'title-x', [], 0]]], [2],
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('parseNoteIdFromResult walks the tree for the note-id UUID', () => {
|
|
43
|
+
const id = '0312fc89-075e-4b3a-810d-141fc8d5af6d';
|
|
44
|
+
expect(parseNoteIdFromResult([[[ [id] ]]])).toBe(id);
|
|
45
|
+
expect(parseNoteIdFromResult({ shell: { noteId: id } })).toBe(id);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('parseNoteIdFromResult ignores non-UUID strings', () => {
|
|
49
|
+
expect(parseNoteIdFromResult([ 'project-id', 'not-a-uuid' ])).toBe('');
|
|
50
|
+
expect(parseNoteIdFromResult(null)).toBe('');
|
|
51
|
+
expect(parseNoteIdFromResult([])).toBe('');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('parseNoteIdFromResult skips the input notebook id before selecting the created note id', () => {
|
|
55
|
+
const notebookId = '17e2b882-6a01-4c6c-9262-0738dfa2abee';
|
|
56
|
+
const noteId = '0312fc89-075e-4b3a-810d-141fc8d5af6d';
|
|
57
|
+
expect(parseNoteIdFromResult([notebookId, [[noteId]]], [notebookId])).toBe(noteId);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('refuses to create a remote note without --execute', async () => {
|
|
61
|
+
const command = getRegistry().get('notebooklm/write-note');
|
|
62
|
+
const page = { goto: vi.fn() };
|
|
63
|
+
await expect(command.func(page, {
|
|
64
|
+
notebook: '17e2b882-6a01-4c6c-9262-0738dfa2abee',
|
|
65
|
+
title: 'Draft note',
|
|
66
|
+
content: 'body',
|
|
67
|
+
})).rejects.toThrow(ArgumentError);
|
|
68
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
});
|
package/clis/pixiv/detail.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { pixivFetch } from './utils.js';
|
|
4
|
+
|
|
5
|
+
function requireIllustBody(body, id) {
|
|
6
|
+
if (!body || Array.isArray(body) || typeof body !== 'object') {
|
|
7
|
+
throw new CommandExecutionError(`Pixiv illustration ${id} returned malformed detail payload`);
|
|
8
|
+
}
|
|
9
|
+
const illustId = String(body.illustId ?? '').trim();
|
|
10
|
+
const title = String(body.illustTitle ?? '').trim();
|
|
11
|
+
const userName = String(body.userName ?? '').trim();
|
|
12
|
+
const userId = String(body.userId ?? '').trim();
|
|
13
|
+
if (!/^\d+$/.test(illustId) || illustId !== id || !title || !userName || !/^\d+$/.test(userId)) {
|
|
14
|
+
throw new CommandExecutionError(`Pixiv illustration ${id} returned malformed detail payload`);
|
|
15
|
+
}
|
|
16
|
+
return { ...body, illustId, illustTitle: title, userName, userId };
|
|
17
|
+
}
|
|
18
|
+
|
|
2
19
|
cli({
|
|
3
20
|
site: 'pixiv',
|
|
4
21
|
name: 'detail',
|
|
@@ -6,7 +23,6 @@ cli({
|
|
|
6
23
|
description: 'View illustration details (tags, stats, URLs)',
|
|
7
24
|
domain: 'www.pixiv.net',
|
|
8
25
|
strategy: Strategy.COOKIE,
|
|
9
|
-
browser: true,
|
|
10
26
|
args: [
|
|
11
27
|
{ name: 'id', required: true, positional: true, help: 'Illustration ID' },
|
|
12
28
|
],
|
|
@@ -23,37 +39,28 @@ cli({
|
|
|
23
39
|
'created',
|
|
24
40
|
'url',
|
|
25
41
|
],
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
likes: b.likeCount,
|
|
51
|
-
views: b.viewCount,
|
|
52
|
-
tags: (b.tags?.tags || []).map(t => t.tag).join(', '),
|
|
53
|
-
created: b.createDate?.split('T')[0] || '',
|
|
54
|
-
url: 'https://www.pixiv.net/artworks/' + b.illustId
|
|
55
|
-
}];
|
|
56
|
-
})()
|
|
57
|
-
` },
|
|
58
|
-
],
|
|
42
|
+
func: async (page, kwargs) => {
|
|
43
|
+
const id = String(kwargs.id ?? '');
|
|
44
|
+
if (!/^\d+$/.test(id)) {
|
|
45
|
+
throw new ArgumentError(`Invalid illustration ID: ${id}`, 'Example: opencli pixiv detail 123456');
|
|
46
|
+
}
|
|
47
|
+
const body = await pixivFetch(page, `/ajax/illust/${id}`, {
|
|
48
|
+
notFoundMsg: `Illustration not found: ${id}`,
|
|
49
|
+
});
|
|
50
|
+
const b = requireIllustBody(body, id);
|
|
51
|
+
return [{
|
|
52
|
+
illust_id: b.illustId,
|
|
53
|
+
title: b.illustTitle,
|
|
54
|
+
author: b.userName,
|
|
55
|
+
user_id: b.userId,
|
|
56
|
+
type: b.illustType === 0 ? 'illust' : b.illustType === 1 ? 'manga' : b.illustType === 2 ? 'ugoira' : String(b.illustType),
|
|
57
|
+
pages: b.pageCount,
|
|
58
|
+
bookmarks: b.bookmarkCount,
|
|
59
|
+
likes: b.likeCount,
|
|
60
|
+
views: b.viewCount,
|
|
61
|
+
tags: (b.tags?.tags || []).map(t => t.tag).join(', '),
|
|
62
|
+
created: b.createDate?.split('T')[0] || '',
|
|
63
|
+
url: `https://www.pixiv.net/artworks/${b.illustId}`,
|
|
64
|
+
}];
|
|
65
|
+
},
|
|
59
66
|
});
|