Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 28 additions & 34 deletions clis/pixiv/detail.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { cli, Strategy } from '@jackwener/opencli/registry';
import { CommandExecutionError } from '@jackwener/opencli/errors';
import { pixivFetch } from './utils.js';
cli({
site: 'pixiv',
name: 'detail',
access: 'read',
description: 'View illustration details (tags, stats, URLs)',
domain: 'www.pixiv.net',
strategy: Strategy.COOKIE,
browser: true,
args: [
{ name: 'id', required: true, positional: true, help: 'Illustration ID' },
],
Expand All @@ -23,37 +24,30 @@ cli({
'created',
'url',
],
pipeline: [
{ navigate: 'https://www.pixiv.net' },
{ evaluate: `(async () => {
const id = \${{ args.id | json }};
const res = await fetch(
'https://www.pixiv.net/ajax/illust/' + id,
{ credentials: 'include' }
);
if (!res.ok) {
if (res.status === 401 || res.status === 403) throw new Error('Authentication required — please log in to Pixiv in Chrome');
if (res.status === 404) throw new Error('Illustration not found: ' + id);
throw new Error('Pixiv request failed (HTTP ' + res.status + ')');
}
const data = await res.json();
const b = data?.body;
if (!b) throw new Error('Illustration not found');
return [{
illust_id: b.illustId,
title: b.illustTitle,
author: b.userName,
user_id: b.userId,
type: b.illustType === 0 ? 'illust' : b.illustType === 1 ? 'manga' : b.illustType === 2 ? 'ugoira' : String(b.illustType),
pages: b.pageCount,
bookmarks: b.bookmarkCount,
likes: b.likeCount,
views: b.viewCount,
tags: (b.tags?.tags || []).map(t => t.tag).join(', '),
created: b.createDate?.split('T')[0] || '',
url: 'https://www.pixiv.net/artworks/' + b.illustId
}];
})()
` },
],
func: async (page, kwargs) => {
const id = String(kwargs.id ?? '');
if (!/^\d+$/.test(id)) {
throw new CommandExecutionError(`Invalid illustration ID: ${id}`);
}
const b = await pixivFetch(page, `/ajax/illust/${id}`, {
notFoundMsg: `Illustration not found: ${id}`,
});
if (!b) {
throw new CommandExecutionError(`Illustration not found: ${id}`);
}
return [{
illust_id: b.illustId,
title: b.illustTitle,
author: b.userName,
user_id: b.userId,
type: b.illustType === 0 ? 'illust' : b.illustType === 1 ? 'manga' : b.illustType === 2 ? 'ugoira' : String(b.illustType),
pages: b.pageCount,
bookmarks: b.bookmarkCount,
likes: b.likeCount,
views: b.viewCount,
tags: (b.tags?.tags || []).map(t => t.tag).join(', '),
created: b.createDate?.split('T')[0] || '',
url: `https://www.pixiv.net/artworks/${b.illustId}`,
}];
},
});
62 changes: 62 additions & 0 deletions clis/pixiv/detail.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { beforeAll, describe, expect, it } from 'vitest';
import { getRegistry } from '@jackwener/opencli/registry';
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { createPageMock } from '../test-utils.js';
import './detail.js';
let cmd;
beforeAll(() => {
cmd = getRegistry().get('pixiv/detail');
expect(cmd?.func).toBeTypeOf('function');
});
describe('pixiv detail', () => {
it('throws CommandExecutionError on invalid illustration ID', async () => {
const page = createPageMock([]);
await expect(cmd.func(page, { id: 'xyz' })).rejects.toThrow(CommandExecutionError);
});
it('throws AuthRequiredError on 401', async () => {
const page = createPageMock([{ __httpError: 401 }]);
await expect(cmd.func(page, { id: '12345' })).rejects.toThrow(AuthRequiredError);
});
it('throws CommandExecutionError on 404', async () => {
const page = createPageMock([{ __httpError: 404 }]);
await expect(cmd.func(page, { id: '12345' })).rejects.toThrow(CommandExecutionError);
});
it('throws CommandExecutionError on non-auth HTTP failure', async () => {
const page = createPageMock([{ __httpError: 500 }]);
await expect(cmd.func(page, { id: '12345' })).rejects.toThrow(CommandExecutionError);
});
it('returns detail row with mapped fields', async () => {
const page = createPageMock([
{
body: {
illustId: '12345',
illustTitle: 'Test Illust',
userName: 'Test Artist',
userId: '99',
illustType: 1,
pageCount: 4,
bookmarkCount: 200,
likeCount: 100,
viewCount: 5000,
tags: { tags: [{ tag: 'original' }, { tag: 'fantasy' }] },
createDate: '2025-01-15T12:00:00+09:00',
},
},
]);
const result = await cmd.func(page, { id: '12345' });
expect(result).toEqual([{
illust_id: '12345',
title: 'Test Illust',
author: 'Test Artist',
user_id: '99',
type: 'manga',
pages: 4,
bookmarks: 200,
likes: 100,
views: 5000,
tags: 'original, fantasy',
created: '2025-01-15',
url: 'https://www.pixiv.net/artworks/12345',
}]);
});
});
57 changes: 26 additions & 31 deletions clis/pixiv/user.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { cli, Strategy } from '@jackwener/opencli/registry';
import { CommandExecutionError } from '@jackwener/opencli/errors';
import { pixivFetch } from './utils.js';
cli({
site: 'pixiv',
name: 'user',
access: 'read',
description: 'View Pixiv artist profile',
domain: 'www.pixiv.net',
strategy: Strategy.COOKIE,
browser: true,
args: [
{ name: 'uid', required: true, positional: true, help: 'Pixiv user ID' },
],
Expand All @@ -21,34 +22,28 @@ cli({
'comment',
'url',
],
pipeline: [
{ navigate: 'https://www.pixiv.net' },
{ evaluate: `(async () => {
const uid = \${{ args.uid | json }};
const res = await fetch(
'https://www.pixiv.net/ajax/user/' + uid + '?full=1',
{ credentials: 'include' }
);
if (!res.ok) {
if (res.status === 401 || res.status === 403) throw new Error('Authentication required — please log in to Pixiv in Chrome');
if (res.status === 404) throw new Error('User not found: ' + uid);
throw new Error('Pixiv request failed (HTTP ' + res.status + ')');
}
const data = await res.json();
const b = data?.body;
if (!b) throw new Error('User not found');
return [{
user_id: uid,
name: b.name,
premium: b.premium ? 'Yes' : 'No',
following: b.following,
illusts: typeof b.illusts === 'object' ? Object.keys(b.illusts).length : (b.illusts || 0),
manga: typeof b.manga === 'object' ? Object.keys(b.manga).length : (b.manga || 0),
novels: typeof b.novels === 'object' ? Object.keys(b.novels).length : (b.novels || 0),
comment: (b.comment || '').slice(0, 80),
url: 'https://www.pixiv.net/users/' + uid
}];
})()
` },
],
func: async (page, kwargs) => {
const uid = String(kwargs.uid ?? '');
if (!/^\d+$/.test(uid)) {
throw new CommandExecutionError(`Invalid user ID: ${uid}`);
}
const b = await pixivFetch(page, `/ajax/user/${uid}`, {
params: { full: 1 },
notFoundMsg: `User not found: ${uid}`,
});
if (!b) {
throw new CommandExecutionError(`User not found: ${uid}`);
}
return [{
user_id: uid,
name: b.name,
premium: b.premium ? 'Yes' : 'No',
following: b.following,
illusts: typeof b.illusts === 'object' ? Object.keys(b.illusts).length : (b.illusts || 0),
manga: typeof b.manga === 'object' ? Object.keys(b.manga).length : (b.manga || 0),
novels: typeof b.novels === 'object' ? Object.keys(b.novels).length : (b.novels || 0),
Comment on lines +42 to +44
comment: (b.comment || '').slice(0, 80),
url: `https://www.pixiv.net/users/${uid}`,
}];
},
});
55 changes: 55 additions & 0 deletions clis/pixiv/user.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { beforeAll, describe, expect, it } from 'vitest';
import { getRegistry } from '@jackwener/opencli/registry';
import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
import { createPageMock } from '../test-utils.js';
import './user.js';
let cmd;
beforeAll(() => {
cmd = getRegistry().get('pixiv/user');
expect(cmd?.func).toBeTypeOf('function');
});
describe('pixiv user', () => {
it('throws CommandExecutionError on invalid user ID', async () => {
const page = createPageMock([]);
await expect(cmd.func(page, { uid: 'abc' })).rejects.toThrow(CommandExecutionError);
});
it('throws AuthRequiredError on 401', async () => {
const page = createPageMock([{ __httpError: 401 }]);
await expect(cmd.func(page, { uid: '11' })).rejects.toThrow(AuthRequiredError);
});
it('throws CommandExecutionError on 404', async () => {
const page = createPageMock([{ __httpError: 404 }]);
await expect(cmd.func(page, { uid: '11' })).rejects.toThrow(CommandExecutionError);
});
it('throws CommandExecutionError on non-auth HTTP failure', async () => {
const page = createPageMock([{ __httpError: 500 }]);
await expect(cmd.func(page, { uid: '11' })).rejects.toThrow(CommandExecutionError);
});
it('returns profile row with computed counts for object-shaped illust fields', async () => {
const page = createPageMock([
{
body: {
name: 'Test Artist',
premium: true,
following: 42,
illusts: { '111': null, '222': null, '333': null },
manga: {},
novels: { '999': null },
comment: 'Hello world',
},
},
]);
const result = await cmd.func(page, { uid: '11' });
expect(result).toEqual([{
user_id: '11',
name: 'Test Artist',
premium: 'Yes',
following: 42,
illusts: 3,
manga: 0,
novels: 1,
comment: 'Hello world',
url: 'https://www.pixiv.net/users/11',
}]);
});
});
Loading