From b8ffb6e5c3f26a8bdc20e65b41f4adcc2eac8f44 Mon Sep 17 00:00:00 2001 From: kyuuseiryuu Date: Sat, 21 Mar 2026 17:06:55 +0900 Subject: [PATCH] refactor(src): switch to Puppeteer-driven account management - Replace `src/Cookie.ts` with direct `Browser` and `Page` management in `Bandai` class. - Implement lazy initialization for the browser instance to avoid redundant startup costs. - Update `loginBandai` to accept an existing `browserAndPage` context rather than creating a new one. - Improve login status verification by checking for the login button ID instead of generic text. - Add `getCartList` to parse cart HTML using `cheerio` and `set-cookie-parser`. - Add `order` and `open` methods to navigate and interact with specific URLs via Puppeteer. - Update `puppeteer-utils` to save cookies automatically on page load. - Remove legacy `src/Cookie.ts` and `src/Cookie.ts` usage. - Bump `set-cookie-parser` dependency to `^3.1.0`. --- bun.lock | 3 ++ package.json | 3 +- src/Bandai.ts | 68 ++++++++++++++++++++++++++--- src/{Cookie.ts => CookieManager.ts} | 28 +++++++++--- src/utils/account-utils.ts | 17 +++++--- src/utils/constants.ts | 1 + src/utils/htmlParser.ts | 15 +++++++ src/utils/index.ts | 2 +- src/utils/puppeteer-utils.ts | 5 +++ test/cart.test.ts | 9 ++++ test/index.ts | 5 +++ test/login.test.ts | 12 ----- 12 files changed, 134 insertions(+), 34 deletions(-) rename src/{Cookie.ts => CookieManager.ts} (54%) create mode 100644 src/utils/htmlParser.ts create mode 100644 test/cart.test.ts delete mode 100644 test/login.test.ts diff --git a/bun.lock b/bun.lock index 719a238..275b037 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "cheerio": "^1.2.0", "puppeteer": "^24.40.0", + "set-cookie-parser": "^3.1.0", }, "devDependencies": { "@types/bun": "latest", @@ -209,6 +210,8 @@ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], diff --git a/package.json b/package.json index 0029745..e74d9ce 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "cheerio": "^1.2.0", - "puppeteer": "^24.40.0" + "puppeteer": "^24.40.0", + "set-cookie-parser": "^3.1.0" } } diff --git a/src/Bandai.ts b/src/Bandai.ts index 082a19b..5891781 100644 --- a/src/Bandai.ts +++ b/src/Bandai.ts @@ -1,16 +1,20 @@ import { fetch } from 'bun'; -import BandaiCookie from './Cookie'; -import { loginBandai } from './utils/account-utils'; +import BandaiCookie from './CookieManager'; +import { loginBandai, updateCookie } from './utils/account-utils'; import * as cheerio from 'cheerio'; import { getBandaiRawCookieRedisKey, redis } from './utils'; import fs from 'fs'; import path from 'path'; +import { parseCartList } from './utils/htmlParser'; +import { initBrowser } from './utils/puppeteer-utils'; +import type { Browser, Page } from 'puppeteer'; export default class Bandai { #email: string; #password: string; #cookieManager: BandaiCookie; #referrer = 'https://p-bandai.jp'; + #browserAndPage?: { browser: Browser; page: Page }; #isInited = false; @@ -20,6 +24,18 @@ export default class Bandai { this.#cookieManager = new BandaiCookie(email); } + async #initBrowserAndPage() { + const browserAndPage = await initBrowser(this.#email); + this.#browserAndPage = browserAndPage; + } + + async getBrowserAndPage() { + if (!this.#browserAndPage) { + await this.#initBrowserAndPage(); + } + return this.#browserAndPage!; + } + async #fetch(url: string, init?: RequestInit) { console.debug('Fetching: %s, cookie inited: %s', url, this.#isInited); if (!this.#isInited) { @@ -42,10 +58,22 @@ export default class Bandai { async #isLogind() { const hasCookie = await redis.get(getBandaiRawCookieRedisKey(this.#email)); if (!hasCookie) return false; - const hasLoginId = await this.#fetch('https://p-bandai.jp/mypage') + const hasLoginBtn = await this.#fetch('https://p-bandai.jp/mypage/') .then(res => res.text()) - .then(html => html.includes('#loign_id')); - return !hasLoginId; + .then(html => { + this.#debugHTML(html, 'mypage-check-login-id') + return html.includes('document.getElementById("btnLogin")'); + }); + if (hasLoginBtn) return false; + const { page } = await this.getBrowserAndPage(); + const mypage = 'https://p-bandai.jp/mypage/'; + await Promise.all([ + page.goto(mypage), + page.waitForNavigation(), + ]); + const hasLogin = page.url() === mypage; + if (!hasLogin) return false; + return true; } #debugHTML(html: string, name: string) { @@ -73,9 +101,37 @@ export default class Bandai { async login() { const isLogin = await this.#isLogind(); if (!isLogin) { - await loginBandai(this.#email, this.#password); + await loginBandai(this.#email, this.#password, await this.getBrowserAndPage()); + await this.#cookieManager.init(); } const id = await this.#getId(); return { email: this.#email, password: this.#password, id }; } + async getCartList() { + const url = 'https://p-bandai.jp/cart'; + const html = await this.#fetch(url) + .then(res => res.text()); + this.#debugHTML(html, 'cart'); + return parseCartList(html); + } + + async order(url: string) { + const { page, browser } = await this.getBrowserAndPage(); + await Promise.all([ + page.goto(url), + page.waitForNavigation(), + page.waitForSelector('#buy'), + ]); + await page.click('#buy'); + await updateCookie(this.#email, browser); + } + async open(url: string) { + const { page, browser } = await this.getBrowserAndPage(); + await Promise.all([ + page.goto(url), + page.waitForNavigation(), + ]); + await updateCookie(this.#email, browser); + Bun.sleep(1000 * 60); + } } \ No newline at end of file diff --git a/src/Cookie.ts b/src/CookieManager.ts similarity index 54% rename from src/Cookie.ts rename to src/CookieManager.ts index 5bd7746..2bdb82c 100644 --- a/src/Cookie.ts +++ b/src/CookieManager.ts @@ -1,5 +1,6 @@ import type { Cookie } from "puppeteer"; import { getBandaiRawCookieRedisKey, redis } from "./utils"; +import { parseSetCookie } from "set-cookie-parser"; export default class BandaiCookie { #cookieMap = new Map(); @@ -17,25 +18,38 @@ export default class BandaiCookie { cookies.map(cookie => { this.#cookieMap.set(cookie.name, cookie); }); - console.debug('Cookie initialized.'); } updateCookie(headers: Headers) { - const cookies = headers.getSetCookie(); + const setCookie = headers.getSetCookie(); + const cookies = parseSetCookie(setCookie); cookies.forEach(cookie => { - const cookieData = cookie.split(';'); // Extract the name=value part of - console.debug(cookieData); - // this.#cookieMap.set(cookie) + const value = this.#cookieMap.get(cookie.name); + if (!value) { + console.debug('Cookie %s not found', cookie.name); + return; + } + this.#cookieMap.set(cookie.name, { + ...value, + value: cookie.value, + domain: cookie.domain ?? value.domain, + expires: cookie.expires?.getTime() || value.expires, + }); }); } getCookie() { - return this.#cookieMap.entries() + return this.#cookieMap.values() .toArray() - .map(([k, v]) => `${k}=${v}`) + .map((v) => `${v.name}=${v.value}`) .join('; '); } + getRawCookie(): Cookie[] { + const cookies = this.#cookieMap.values().toArray(); + return cookies; + } + async saveCookie() { const cookieString = JSON.stringify(this.#cookieMap.values().toArray()); await redis.set(getBandaiRawCookieRedisKey(this.#id), cookieString); diff --git a/src/utils/account-utils.ts b/src/utils/account-utils.ts index a4f1bb1..f5c95eb 100644 --- a/src/utils/account-utils.ts +++ b/src/utils/account-utils.ts @@ -1,15 +1,21 @@ +import type { Browser, Page } from "puppeteer"; import { getBandaiRawCookieRedisKey, redis } from "."; -import { initBrowser } from "./puppeteer-utils"; -export async function loginBandai(emai: string, password: string) { - const { browser, page } = await initBrowser(emai); +export async function updateCookie(email: string, browser: Browser) { + const rawCookies = await browser.cookies(); + await redis.set(getBandaiRawCookieRedisKey(email), JSON.stringify(rawCookies)); +} + + +export async function loginBandai(email: string, password: string, browserAndPage: { browser: Browser, page: Page; }) { + const { page } = browserAndPage; const url = 'https://p-bandai.jp/login/'; await Promise.all([ page.goto(url), page.waitForNavigation(), ]) await Promise.all([ - page.type('#login_id', emai), + page.type('#login_id', email), Bun.sleep(3000), // wait for a second ]); await Promise.all([ @@ -20,8 +26,5 @@ export async function loginBandai(emai: string, password: string) { page.click('#btnLogin'), page.waitForNavigation(), ]); - const rawCookies = await browser.cookies(); - await redis.set(getBandaiRawCookieRedisKey(emai), JSON.stringify(rawCookies)); - await browser.close() return; } \ No newline at end of file diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 4af53bc..bc2502e 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -15,6 +15,7 @@ export const puppeteerInitArgs = [ export const CHROME = { Windows: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', + // Windows: 'C:\\Users\\kyuus\\.cache\\puppeteer\\chrome\\win64-146.0.7680.153\\chrome-win64\\chrome.exe', // Linux: '/usr/bin/google-chrome', Linux: '/usr/bin/chromium', Mac: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', diff --git a/src/utils/htmlParser.ts b/src/utils/htmlParser.ts new file mode 100644 index 0000000..2ad0a11 --- /dev/null +++ b/src/utils/htmlParser.ts @@ -0,0 +1,15 @@ +import * as cheerio from 'cheerio'; + +export function parseCartList(html: string) { + const $ = cheerio.load(html); + const orderList = $('.pbCartOrderList > dl').toArray(); + const items = orderList.map(dl => { + const a = $(dl).find('dt > a'); + const title = a.text().trim(); + const url = a.attr('href') ?? ''; + const id = /item-(?\d+)\/$/.exec(url)?.groups?.id ?? ''; + const img = $(dl).find('.pbCartOrderList-img img').attr('src') ?? ''; + return { title, url, id, img }; + }); + return items; +} \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index 7ad0f01..7a1b228 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -11,4 +11,4 @@ export const getChromePath = () => { if (platform === 'linux') return CHROME.Linux; if (platform === 'darwin') return CHROME.Mac; throw new Error('Unsupported platform: ' + platform); -}; \ No newline at end of file +}; diff --git a/src/utils/puppeteer-utils.ts b/src/utils/puppeteer-utils.ts index c7a1fd0..1da8516 100644 --- a/src/utils/puppeteer-utils.ts +++ b/src/utils/puppeteer-utils.ts @@ -4,6 +4,7 @@ import fs from "fs"; import path from "path"; import { puppeteerInitArgs } from "./constants"; import { getChromePath } from "."; +import { updateCookie } from "./account-utils"; export async function setupNewPage(page: Page) { await page.evaluateOnNewDocument(() => { @@ -38,6 +39,10 @@ export async function initBrowser(id: string) { .then(pages => pages[0] || browser.newPage()) .then(page => page); setupNewPage(page); + page.on('load', async () => { + console.debug('page loade: %s', page.url()); + updateCookie(id, browser); + }); return { browser, page, diff --git a/test/cart.test.ts b/test/cart.test.ts new file mode 100644 index 0000000..7829c06 --- /dev/null +++ b/test/cart.test.ts @@ -0,0 +1,9 @@ +import { test, expect } from "bun:test"; +import path from 'path'; +import { parseCartList } from "../src/utils/htmlParser"; + +test('Load cart list', async () => { + const html = await Bun.file(path.resolve(__dirname, '../', 'debug-html', 'allin_603_outlook_com', 'cart.html')).text(); + const result = parseCartList(html); + expect(result).toBeDefined(); +}); \ No newline at end of file diff --git a/test/index.ts b/test/index.ts index 9c9ee26..623620a 100644 --- a/test/index.ts +++ b/test/index.ts @@ -6,6 +6,11 @@ async function main() { const account = new Bandai(email, password); const accountInfo = await account.login(); console.debug('Account', accountInfo); + const url = 'https://p-bandai.jp/item/item-1000245579/?spec=pc21&cref=500894283&click_recom=1'; + await account.order(url); + const cartList = await account.getCartList(); + console.debug('Cart', cartList); + await account.open('https://p-bandai.jp/cart'); } main().then(() => { diff --git a/test/login.test.ts b/test/login.test.ts deleted file mode 100644 index ad739c8..0000000 --- a/test/login.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { test, expect } from "bun:test"; -import Bandai from "../src/Bandai"; -import { getBandaiRawCookieRedisKey, redis } from "../src/utils"; - -test('Test login', async () => { - const email = 'allin-603@outlook.com'; - const password = '123456789qw'; - const account = new Bandai(email, password); - await account.login(); - const cookie = await redis.get(getBandaiRawCookieRedisKey(email)); - expect(cookie).toBeDefined(); -}); \ No newline at end of file