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`.
This commit is contained in:
kyuuseiryuu 2026-03-21 14:43:51 +09:00
parent abec190d1d
commit 83e9307011
14 changed files with 195 additions and 83 deletions

1
.gitignore vendored
View File

@ -34,3 +34,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.DS_Store
user_data/
debug-html/

View File

@ -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=="],

View File

@ -11,6 +11,7 @@
},
"dependencies": {
"cheerio": "^1.2.0",
"puppeteer": "^24.40.0"
"puppeteer": "^24.40.0",
"set-cookie-parser": "^3.1.0"
}
}

View File

@ -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);
}
}

View File

@ -1,44 +0,0 @@
import { getBandaiCookieRedisKey, redis } from "./utils";
export default class BandaiCookie {
#cookieMap = new Map<string, string>();
#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);
}
}

57
src/CookieManager.ts Normal file
View File

@ -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<string, Cookie>();
#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);
}
}

View File

@ -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;
}

View File

@ -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',
};

15
src/utils/htmlParser.ts Normal file
View File

@ -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-(?<id>\d+)\/$/.exec(url)?.groups?.id ?? '';
const img = $(dl).find('.pbCartOrderList-img img').attr('src') ?? '';
return { title, url, id, img };
});
return items;
}

View File

@ -3,7 +3,8 @@ 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;

View File

@ -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,

9
test/cart.test.ts Normal file
View File

@ -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();
});

View File

@ -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(() => {

View File

@ -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();
});