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:
parent
abec190d1d
commit
83e9307011
1
.gitignore
vendored
1
.gitignore
vendored
@ -34,3 +34,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
.DS_Store
|
||||
|
||||
user_data/
|
||||
debug-html/
|
||||
3
bun.lock
3
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=="],
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"cheerio": "^1.2.0",
|
||||
"puppeteer": "^24.40.0"
|
||||
"puppeteer": "^24.40.0",
|
||||
"set-cookie-parser": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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
57
src/CookieManager.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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
15
src/utils/htmlParser.ts
Normal 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;
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
};
|
||||
|
||||
@ -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
9
test/cart.test.ts
Normal 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();
|
||||
});
|
||||
@ -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(() => {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user