From 83e930701128206c60e8c5f3f0aa8b1e130e0bda Mon Sep 17 00:00:00 2001 From: kyuuseiryuu Date: Sat, 21 Mar 2026 14:43:51 +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`. --- .gitignore | 1 + bun.lock | 3 ++ package.json | 3 +- src/Bandai.ts | 90 ++++++++++++++++++++++++++++++++---- src/Cookie.ts | 44 ------------------ src/CookieManager.ts | 57 +++++++++++++++++++++++ src/utils/account-utils.ts | 25 +++++----- src/utils/constants.ts | 4 +- src/utils/htmlParser.ts | 15 ++++++ src/utils/index.ts | 5 +- src/utils/puppeteer-utils.ts | 5 ++ test/cart.test.ts | 9 ++++ test/index.ts | 5 ++ test/login.test.ts | 12 ----- 14 files changed, 195 insertions(+), 83 deletions(-) delete mode 100644 src/Cookie.ts create mode 100644 src/CookieManager.ts create mode 100644 src/utils/htmlParser.ts create mode 100644 test/cart.test.ts delete mode 100644 test/login.test.ts diff --git a/.gitignore b/.gitignore index eff015e..3ec6933 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json .DS_Store user_data/ +debug-html/ \ No newline at end of file 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 5d5562d..5891781 100644 --- a/src/Bandai.ts +++ b/src/Bandai.ts @@ -1,14 +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 { getBandaiCookieRedisKey, redis } from './utils'; +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; @@ -18,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) { @@ -38,19 +56,43 @@ export default class Bandai { } async #isLogind() { - const hasCookie = await redis.get(getBandaiCookieRedisKey(this.#email)); + 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) { + const dir = path.resolve(__dirname, '../debug-html', this.#email.replace(/\W/g, '_')); + const file = path.resolve(dir, `${name}.html`); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(file, html); } async #getId() { - const selector = 'body > div.pb24-wrap > div.pb24-mypage-content > div.pb24-mypage-content__side > div:nth-child(1) > dl > dd:nth-child(2)'; + const selector = '#headerMenu .pb24-header-menu__data--name > dd'; const id = await this.#fetch('https://p-bandai.jp/mypage/') .then(res => res.text()) - .then(html => cheerio.load(html)) + .then(html => { + this.#debugHTML(html, 'my-page'); + return cheerio.load(html); + }) .then($ => { return $(selector).text().trim(); }); @@ -59,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/Cookie.ts deleted file mode 100644 index aa7d8b4..0000000 --- a/src/Cookie.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { getBandaiCookieRedisKey, redis } from "./utils"; - -export default class BandaiCookie { - #cookieMap = new Map(); - - #id: string; - - constructor(email: string) { - this.#id = email; - } - - async init() { - const cache = await redis.get(getBandaiCookieRedisKey(this.#id)); - if (!cache) return; - cache.split(';').forEach((cookie) => { - const [key, value] = cookie.trim().split('='); - if (!key || !value) return; - this.#cookieMap.set(key, value); - }); - console.debug('Cookie initialized.'); - } - - updateCookie(headers: Headers) { - const cookies = headers.getSetCookie(); - const newCookies = cookies.map(cookie => (cookie.split(';')[0] ?? '').trim()).filter(Boolean); - newCookies.forEach(cookie => { - const [key, value] = cookie.trim().split('='); - if (!key || !value) return; - this.#cookieMap.set(key.trim(), value.trim()); - }); - } - - getCookie() { - return this.#cookieMap.entries() - .toArray() - .map(([k, v]) => `${k}=${v}`) - .join('; '); - } - - async saveCookie() { - const cookieString = this.getCookie(); - await redis.set(getBandaiCookieRedisKey(this.#id), cookieString); - } -} \ No newline at end of file diff --git a/src/CookieManager.ts b/src/CookieManager.ts new file mode 100644 index 0000000..2bdb82c --- /dev/null +++ b/src/CookieManager.ts @@ -0,0 +1,57 @@ +import type { Cookie } from "puppeteer"; +import { getBandaiRawCookieRedisKey, redis } from "./utils"; +import { parseSetCookie } from "set-cookie-parser"; + +export default class BandaiCookie { + #cookieMap = new Map(); + + #id: string; + + constructor(email: string) { + this.#id = email; + } + + async init() { + const cache = await redis.get(getBandaiRawCookieRedisKey(this.#id)); + if (!cache) return; + const cookies: Cookie[] = JSON.parse(cache); + cookies.map(cookie => { + this.#cookieMap.set(cookie.name, cookie); + }); + } + + updateCookie(headers: Headers) { + const setCookie = headers.getSetCookie(); + const cookies = parseSetCookie(setCookie); + cookies.forEach(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.values() + .toArray() + .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); + } +} \ No newline at end of file diff --git a/src/utils/account-utils.ts b/src/utils/account-utils.ts index c3fc4ac..f5c95eb 100644 --- a/src/utils/account-utils.ts +++ b/src/utils/account-utils.ts @@ -1,15 +1,21 @@ -import { getBandaiCookieRedisKey, redis } from "."; -import { initBrowser } from "./puppeteer-utils"; +import type { Browser, Page } from "puppeteer"; +import { getBandaiRawCookieRedisKey, redis } from "."; -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,12 +26,5 @@ export async function loginBandai(emai: string, password: string) { page.click('#btnLogin'), page.waitForNavigation(), ]); - const cookies = await browser.cookies() - .then(cookies => { - return cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; '); - }); - console.debug('Cookie', cookies); - await redis.set(getBandaiCookieRedisKey(emai), cookies); - await browser.close() - return cookies; + return; } \ No newline at end of file diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 93f5fa4..bc2502e 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -15,6 +15,8 @@ export const puppeteerInitArgs = [ export const CHROME = { Windows: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', - Linux: '/usr/bin/google-chrome', + // 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', }; \ No newline at end of file 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 ffc3fba..7a1b228 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,11 +3,12 @@ import { RedisClient } from "bun"; import { CHROME } from "./constants"; export const redis = new RedisClient(process.env.REDIS); -export const getBandaiCookieRedisKey = (email: string) => `bandai:cookies:${email}`; +export const getBandaiRawCookieRedisKey = (email: string) => `bandai:cookies:${email}:raw`; + export const getChromePath = () => { const platform = process.platform; if (platform === 'win32') return CHROME.Windows; 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 48b6e6a..0000000 --- a/test/login.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { test, expect } from "bun:test"; -import Bandai from "../src/Bandai"; -import { getBandaiCookieRedisKey, 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(getBandaiCookieRedisKey(email)); - expect(cookie).toBeDefined(); -}); \ No newline at end of file