UNPKG

@book000/pixivts

Version:

pixiv Unofficial API Library for TypeScript

698 lines 24 kB
"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