@book000/pixivts
Version:
pixiv Unofficial API Library for TypeScript
698 lines • 24 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_crypto_1 = __importDefault(require("node:crypto"));
const axios_1 = __importDefault(require("axios"));
const qs_1 = __importDefault(require("qs"));
const options_1 = require("./options");
const saving_responses_1 = require("./saving-responses");
/**
* pixiv API
*/
class Pixiv {
/**
* コンストラクタ。外部からインスタンス化できないので、of メソッドを使うこと。
*
* @param userId ユーザー ID
* @param accessToken アクセストークン
* @param refreshToken リフレッシュトークン
* @param pixivTsOptions Pixivts オプション
*/
constructor(userId, accessToken, refreshToken, responseDatabase) {
this.hosts = 'https://app-api.pixiv.net';
this.userId = userId;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.responseDatabase = responseDatabase ?? null;
this.axios = axios_1.default.create({
baseURL: this.hosts,
headers: {
Host: 'app-api.pixiv.net',
'App-OS': 'ios',
'App-OS-Version': '14.6',
'User-Agent': 'PixivIOSApp/7.13.3 (iOS 14.6; iPhone13,2)',
Authorization: `Bearer ${this.accessToken}`,
},
validateStatus: () => true,
});
}
/**
* リフレッシュトークンからインスタンスを生成する。
*
* @param refreshToken リフレッシュトークン
* @returns Pixiv インスタンス
*/
static async of(refreshToken, pixivTsOptions) {
// @see https://github.com/upbit/pixivpy/blob/master/pixivpy3/api.py#L120
// UTCで YYYY-MM-DDTHH:mm:ss+00:00 の形式で現在時刻を取得
const localTime = new Date().toISOString().replace(/Z$/, '+00:00');
const headers = {
'x-client-time': localTime,
'x-client-hash': this.hash(localTime),
'app-os': 'ios',
'app-os-version': '16.4.1',
'user-agent': ' PixivIOSApp/7.16.9 (iOS 16.4.1; iPad13,4)',
header: 'application/x-www-form-urlencoded',
};
const authUrl = 'https://oauth.secure.pixiv.net/auth/token';
const data = qs_1.default.stringify({
client_id: this.clientId,
client_secret: this.clientSecret,
get_secure_url: 1,
grant_type: 'refresh_token',
refresh_token: refreshToken,
});
const response = await axios_1.default.post(authUrl, data, {
headers,
validateStatus: () => true,
});
if (response.status !== 200) {
throw new Error('Failed to refresh token');
}
const options = {
userId: response.data.user.id,
accessToken: response.data.response.access_token,
refreshToken: response.data.response.refresh_token,
};
const responseDatabase = pixivTsOptions?.debugOptions?.outputResponse
?.enable
? new saving_responses_1.ResponseDatabase(pixivTsOptions.debugOptions.outputResponse.db)
: null;
if (responseDatabase) {
await responseDatabase.init();
await responseDatabase.migrate();
await responseDatabase.sync();
}
return new Pixiv(options.userId, options.accessToken, options.refreshToken, responseDatabase);
}
/**
* 画像のaxiosストリームを取得する。
*/
static async getAxiosImageStream(url) {
return axios_1.default.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36',
Referer: 'https://www.pixiv.net/',
},
responseType: 'stream',
});
}
// ---------- イラスト ---------- //
/**
* イラストの詳細情報を取得する。
*
* @param options オプション
* @returns レスポンス
*/
async illustDetail(options) {
const parameters = {
...this.convertCamelToSnake(options),
illust_id: options.illustId,
};
return this.request({
method: 'GET',
path: '/v1/illust/detail',
params: parameters,
});
}
/**
* イラストの関連イラストを取得する。
*
* @param options オプション
* @returns レスポンス
*/
async illustRelated(options) {
this.checkRequiredOptions(options, ['illustId']);
const parameters = {
...this.convertCamelToSnake(options),
illust_id: options.illustId,
seed_illust_ids: options.seedIllustIds,
viewed: options.viewed,
offset: options.offset,
};
return this.request({
method: 'GET',
path: '/v2/illust/related',
params: parameters,
});
}
/**
* イラストを検索する。
*
* @param options オプション
* @returns レスポンス
*/
async searchIllust(options) {
this.checkRequiredOptions(options, ['word']);
const parameters = {
...this.convertCamelToSnake(options),
word: options.word, // required
search_target: options.searchTarget ?? options_1.SearchTarget.PARTIAL_MATCH_FOR_TAGS,
sort: options.sort ?? options_1.SearchSort.DATE_DESC,
start_date: options.startDate,
end_date: options.endDate,
filter: options.filter ?? options_1.OSFilter.FOR_IOS,
offset: options.offset,
merge_plain_keyword_results: options.mergePlainKeywordResults ?? true,
include_translated_tag_results: options.includeTranslatedTagResults ?? true,
};
return this.request({
method: 'GET',
path: '/v1/search/illust',
params: parameters,
});
}
/**
* イラストランキングを取得する。
*
* @param options オプション
* @returns レスポンス
*/
async illustRanking(options = {}) {
const parameters = {
...this.convertCamelToSnake(options),
mode: options.mode ?? options_1.RankingMode.DAY,
filter: options.filter ?? options_1.OSFilter.FOR_IOS,
date: options.date ?? undefined,
offset: options.offset ?? undefined,
};
return this.request({
method: 'GET',
path: '/v1/illust/ranking',
params: parameters,
});
}
/**
* おすすめイラストを取得する。
*
* @param options オプション
* @returns レスポンス
*/
async illustRecommended(options = {}) {
const parameters = {
...this.convertCamelToSnake(options),
filter: options.filter ?? options_1.OSFilter.FOR_IOS,
include_ranking_illusts: options.includeRankingIllusts ?? true,
min_bookmark_id_for_recent_illust: options.minBookmarkIdForRecentIllust ?? undefined,
max_bookmark_id_for_recommend: options.maxBookmarkIdForRecommend ?? undefined,
offset: options.offset ?? undefined,
include_privacy_policy: options.includePrivacyPolicy ?? true,
};
return this.request({
method: 'GET',
path: '/v1/illust/recommended',
params: parameters,
});
}
/**
* イラストシリーズの詳細情報を取得する。
*
* @param options オプション
* @returns レスポンス
*/
async illustSeries(options) {
this.checkRequiredOptions(options, ['illustSeriesId']);
const parameters = {
...this.convertCamelToSnake(options),
illust_series_id: options.illustSeriesId,
filter: options.filter ?? options_1.OSFilter.FOR_IOS,
// offset: options.offset,
};
return this.request({
method: 'GET',
path: '/v1/illust/series',
params: parameters,
});
}
/**
* イラストをブックマークする。
*
* @param options オプション
* @returns レスポンス
*/
async illustBookmarkAdd(options) {
this.checkRequiredOptions(options, ['illustId']);
const data = {
...this.convertCamelToSnake(options),
illust_id: options.illustId,
restrict: options.restrict ?? options_1.BookmarkRestrict.PUBLIC,
tags: options.tags ?? [],
};
return this.request({
method: 'POST',
path: '/v2/illust/bookmark/add',
data,
});
}
/**
* イラストのブックマークを削除する。
*
* @param options オプション
* @returns レスポンス
*/
async illustBookmarkDelete(options) {
this.checkRequiredOptions(options, ['illustId']);
const data = {
...this.convertCamelToSnake(options),
illust_id: options.illustId,
};
return this.request({
method: 'POST',
path: '/v1/illust/bookmark/delete',
data,
});
}
// ---------- マンガ ---------- //
async mangaRecommended(options = {}) {
const parameters = {
...this.convertCamelToSnake(options),
filter: options.filter ?? options_1.OSFilter.FOR_IOS,
include_ranking_illusts: options.includeRankingIllusts ?? true,
max_bookmark_id: options.maxBookmarkId ?? undefined,
offset: options.offset ?? undefined,
include_privacy_policy: options.includePrivacyPolicy ?? true,
};
return this.request({
method: 'GET',
path: '/v1/manga/recommended',
params: parameters,
});
}
// ---------- うごイラ ---------- //
/**
* うごイラの詳細情報を取得する。
*
* @param options オプション
* @returns レスポンス
*/
async ugoiraMetadata(options) {
const parameters = {
...this.convertCamelToSnake(options),
illust_id: options.illustId,
};
return this.request({
method: 'GET',
path: '/v1/ugoira/metadata',
params: parameters,
});
}
// ---------- 小説 ---------- //
/**
* 小説の詳細情報を取得する。
*
* @param options オプション
* @returns レスポンス
*/
async novelDetail(options) {
this.checkRequiredOptions(options, ['novelId']);
const parameters = {
...this.convertCamelToSnake(options),
novel_id: options.novelId,
};
return this.request({
method: 'GET',
path: '/v2/novel/detail',
params: parameters,
});
}
/**
* 小説の本文を取得する。
*
* @param options オプション
* @returns レスポンス
*/
async novelText(options) {
this.checkRequiredOptions(options, ['id']);
const parameters = {
...this.convertCamelToSnake(options),
id: options.id,
};
return await this.request({
method: 'GET',
path: '/webview/v2/novel',
params: parameters,
});
}
/**
* 小説の関連小説を取得する。
*
* @param options オプション
* @returns レスポンス
*/
async novelRelated(options) {
this.checkRequiredOptions(options, ['novelId']);
const parameters = {
...this.convertCamelToSnake(options),
novel_id: options.novelId,
seed_novel_ids: options.seedNovelIds,
viewed: options.viewed,
};
return this.request({
method: 'GET',
path: '/v1/novel/related',
params: parameters,
});
}
/**
* 小説を検索する。
*
* @param options オプション
* @returns レスポンス
*/
async searchNovel(options) {
this.checkRequiredOptions(options, ['word']);
const parameters = {
...this.convertCamelToSnake(options),
word: options.word, // required
search_target: options.searchTarget ?? options_1.SearchTarget.PARTIAL_MATCH_FOR_TAGS,
sort: options.sort ?? options_1.SearchSort.DATE_DESC,
startDate: options.startDate,
endDate: options.endDate,
filter: options.filter ?? options_1.OSFilter.FOR_IOS,
offset: options.offset,
merge_plain_keyword_results: options.mergePlainKeywordResults ?? true,
include_translated_tag_results: options.includeTranslatedTagResults ?? true,
};
return this.request({
method: 'GET',
path: '/v1/search/novel',
params: parameters,
});
}
/**
* 小説ランキングを取得する。
*/
async novelRanking(options = {}) {
const parameters = {
...this.convertCamelToSnake(options),
mode: options.mode ?? options_1.RankingMode.DAY,
date: options.date ?? undefined,
offset: options.offset ?? undefined,
};
return this.request({
method: 'GET',
path: '/v1/novel/ranking',
params: parameters,
});
}
/**
* おすすめ小説を取得する。
*
* @param options オプション
* @returns レスポンス
*/
async novelRecommended(options = {}) {
const parameters = {
...this.convertCamelToSnake(options),
// filter: options.filter ?? 'for_ios',
include_ranking_novels: options.includeRankingNovels ?? true,
already_recommended: options.alreadyRecommended
? options.alreadyRecommended.join(',')
: undefined,
max_bookmark_id_for_recommend: options.maxBookmarkIdForRecommend ?? undefined,
offset: options.offset ?? undefined,
include_privacy_policy: options.includePrivacyPolicy ?? true,
};
return this.request({
method: 'GET',
path: '/v1/novel/recommended',
params: parameters,
});
}
/**
* 小説シリーズの詳細情報を取得する。
*
* @param options オプション
* @returns レスポンス
*/
async novelSeries(options) {
this.checkRequiredOptions(options, ['seriesId']);
const parameters = {
...this.convertCamelToSnake(options),
series_id: options.seriesId,
// filter: options.filter ?? 'for_ios',
last_order: options.lastOrder ?? undefined,
};
return this.request({
method: 'GET',
path: '/v2/novel/series',
params: parameters,
});
}
/**
* 小説をブックマークする。
*
* @param options オプション
* @returns レスポンス
*/
async novelBookmarkAdd(options) {
this.checkRequiredOptions(options, ['novelId']);
const data = {
...this.convertCamelToSnake(options),
novel_id: options.novelId,
restrict: options.restrict ?? options_1.BookmarkRestrict.PUBLIC,
tags: options.tags ?? [],
};
return this.request({
method: 'POST',
path: '/v2/novel/bookmark/add',
data,
});
}
/**
* 小説のブックマークを削除する。
*
* @param options オプション
* @returns レスポンス
*/
async novelBookmarkDelete(options) {
this.checkRequiredOptions(options, ['novelId']);
const data = {
...this.convertCamelToSnake(options),
novel_id: options.novelId,
};
return this.request({
method: 'POST',
path: '/v1/novel/bookmark/delete',
data,
});
}
// ---------- ユーザ ---------- //
/**
* ユーザの詳細情報を取得する。
*
* @param options オプション
* @returns レスポンス
*/
async userDetail(options) {
this.checkRequiredOptions(options, ['userId']);
const parameters = {
...this.convertCamelToSnake(options),
user_id: options.userId,
filter: options.filter ?? options_1.OSFilter.FOR_IOS,
};
return this.request({
method: 'GET',
path: '/v1/user/detail',
params: parameters,
});
}
/**
* ユーザのイラストブックマークを取得する。
*
* @param options オプション
* @returns レスポンス
*/
async userBookmarksIllust(options) {
this.checkRequiredOptions(options, ['userId']);
const parameters = {
...this.convertCamelToSnake(options),
user_id: options.userId,
restrict: options.restrict ?? options_1.BookmarkRestrict.PUBLIC,
filter: options.filter ?? options_1.OSFilter.FOR_IOS,
max_bookmark_id: options.maxBookmarkId ?? undefined,
tag: options.tag ?? undefined,
};
return this.request({
method: 'GET',
path: '/v1/user/bookmarks/illust',
params: parameters,
});
}
/**
* ユーザの小説ブックマークを取得する。
*
* @param options オプション
* @returns レスポンス
*/
async userBookmarksNovel(options) {
this.checkRequiredOptions(options, ['userId']);
const parameters = {
...this.convertCamelToSnake(options),
user_id: options.userId,
restrict: options.restrict ?? options_1.BookmarkRestrict.PUBLIC,
max_bookmark_id: options.maxBookmarkId ?? undefined,
tag: options.tag ?? undefined,
};
return this.request({
method: 'GET',
path: '/v1/user/bookmarks/novel',
params: parameters,
});
}
// ---------- その他 ---------- //
/**
* 接続を閉じる。
*/
async close() {
if (this.responseDatabase) {
await this.responseDatabase.close();
}
}
// ---------- ユーティリティ ---------- //
/**
* クエリストリングをパースする。
*
* @param url URL
* @returns パースしたクエリストリングオブジェクト
*/
static parseQueryString(url) {
let query = url;
if (url.includes('?')) {
query = url.split('?')[1];
}
return qs_1.default.parse(query);
}
/**
* レスポンスがエラーかどうかを判定する。
*
* @param response Axios レスポンス
* @returns エラーかどうか
*/
static isError(response) {
return (
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
response.error !== undefined &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
response.error.user_message !== undefined &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
response.error.message !== undefined &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
response.error.reason !== undefined);
}
/**
* MD5ハッシュを生成する。
*
* @param str 文字列
* @returns ハッシュ
*/
static hash(string) {
const hash = node_crypto_1.default.createHash('md5');
return hash.update(string + this.hashSecret).digest('hex');
}
/**
* リクエストを送信する。
*
* ジェネリクスの順番は、T: リクエスト、U: レスポンス。
*
* @param options オプション
* @returns レスポンス
*/
async request(options) {
if (options.method === 'GET') {
return await this.saveResponse(options, await this.axios.get(options.path, {
params: options.params,
paramsSerializer: { indexes: true },
}));
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (options.method === 'POST') {
return await this.saveResponse(options, await this.axios.post(options.path, qs_1.default.stringify(options.data), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
paramsSerializer: { indexes: true },
}));
}
throw new Error('Invalid method');
}
async saveResponse(request, response) {
if (this.responseDatabase === null) {
return response;
}
const method = request.method;
const path = request.path;
const rawUrl = [
this.hosts,
path,
method === 'GET'
? qs_1.default.stringify(request.params, { addQueryPrefix: true })
: '',
].join('');
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const url = response.request.res.responseUrl ?? rawUrl;
const responseType = this.isJSON(response.data) ? 'JSON' : 'TEXT';
const responseBody = responseType === 'JSON'
? JSON.stringify(response.data)
: response.data;
await this.responseDatabase.addResponse({
method,
endpoint: path,
url,
requestHeaders: JSON.stringify(response.config.headers),
requestBody: response.config.data,
responseType,
statusCode: response.status,
responseHeaders: JSON.stringify(response.headers),
responseBody,
});
return response;
}
isJSON(value) {
if (typeof value === 'object') {
return true;
}
try {
JSON.parse(value);
return true;
}
catch {
return false;
}
}
/**
* 必須のオプションが含まれているかどうかをチェックする。
*
* @param options オプション
* @param required 必須のオプションキー
* @throws 必須のオプションが含まれていない場合
*/
checkRequiredOptions(options, required) {
for (const key of required) {
if (options[key] === undefined) {
throw new Error(`Missing required option: ${key}`);
}
}
}
/**
* キャメルケースのオブジェクトキーをスネークケースなオブジェクトキーに変換する。
*
* @param object オブジェクト
* @returns 変換後のオブジェクト
*/
convertCamelToSnake(object) {
const result = {};
for (const key of Object.keys(object)) {
const snakeKey = key.replaceAll(/([A-Z])/g, (m) => `_${m[0].toLowerCase()}`);
result[snakeKey] = object[key];
}
return result;
}
}
Pixiv.clientId = 'MOBrBDS8blbauoSck0ZfDbtuzpyT';
Pixiv.clientSecret = 'lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj';
Pixiv.hashSecret = '28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c';
exports.default = Pixiv;
//# sourceMappingURL=pixiv.js.map