refactor(utils): split shared helpers and server scraping modules
- move event scraping/parsing and match lookup logic into src/utils/server.ts - extract BASE_URL and grouping/round-table helpers into src/utils/common.ts - update component/service entry imports to new module locations - remove deprecated src/utils/utils.ts monolith - clean up unused XCXFindUser type import - update tests to new utils import path
This commit is contained in:
parent
79c839db00
commit
b7d2109f2e
@ -1,7 +1,7 @@
|
|||||||
import { expect, test } from 'bun:test';
|
import { expect, test } from 'bun:test';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { fetchEventContentHTML, fetchEventListHTML } from '../src/utils/utils';
|
import { fetchEventContentHTML, fetchEventListHTML } from '../src/utils/';
|
||||||
|
|
||||||
test('load html', async () => {
|
test('load html', async () => {
|
||||||
const saveTo = path.resolve(__dirname, 'data', 'view-event.html');
|
const saveTo = path.resolve(__dirname, 'data', 'view-event.html');
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { expect, test } from 'bun:test';
|
import { expect, test } from 'bun:test';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { parseEventInfo, parseEventList, sneckGroup } from '../src/utils/utils';
|
import { parseEventInfo, parseEventList, sneckGroup } from '../src/utils/';
|
||||||
|
|
||||||
|
|
||||||
const matchId = '167684';
|
const matchId = '167684';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Button, Card, Divider, Drawer, Flex, Space, Table } from "antd";
|
import { Button, Card, Divider, Drawer, Flex, Space, Table } from "antd";
|
||||||
import type { BasePlayer } from "../types";
|
import type { BasePlayer } from "../types";
|
||||||
import { getRoundTable } from "../utils/utils";
|
import { getRoundTable } from "../utils/common";
|
||||||
import User from "./User";
|
import User from "./User";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Flex, Form, InputNumber, Segmented, Switch } from "antd";
|
|||||||
import { chunk } from 'lodash';
|
import { chunk } from 'lodash';
|
||||||
import type { BasePlayer } from "../types";
|
import type { BasePlayer } from "../types";
|
||||||
import { GroupMember } from "./GroupMember";
|
import { GroupMember } from "./GroupMember";
|
||||||
import { sneckGroup } from "../utils/utils";
|
import { sneckGroup } from "../utils/common";
|
||||||
|
|
||||||
interface Player extends BasePlayer {
|
interface Player extends BasePlayer {
|
||||||
nowScore?: string;
|
nowScore?: string;
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { serve } from "bun";
|
import { serve } from "bun";
|
||||||
import { getMatchInfo, listEvent } from "./utils/utils";
|
import { getMatchInfo, listEvent, xcxApi } from "./utils/server";
|
||||||
import index from "./index.html";
|
import index from "./index.html";
|
||||||
import { xcxApi } from "./utils/server";
|
|
||||||
import { getUidScore } from "./services/uidScoreStore";
|
import { getUidScore } from "./services/uidScoreStore";
|
||||||
|
|
||||||
if (!process.env.KAIQIUCC_TOKEN) {
|
if (!process.env.KAIQIUCC_TOKEN) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { GamesData, XCXFindUser, XCXFindUserResp, XCXMember, XCXProfile, XCXTag } from "../types";
|
import type { GamesData, XCXFindUserResp, XCXMember, XCXProfile, XCXTag } from "../types";
|
||||||
import { BASE_URL } from "../utils/utils";
|
import { BASE_URL } from "../utils/common";
|
||||||
|
|
||||||
const XCX_BASE_URL = `${BASE_URL}/xcx/public/index.php`;
|
const XCX_BASE_URL = `${BASE_URL}/xcx/public/index.php`;
|
||||||
|
|
||||||
|
|||||||
47
src/utils/common.ts
Normal file
47
src/utils/common.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { chunk } from "lodash";
|
||||||
|
|
||||||
|
export const BASE_URL = `https://kaiqiuwang.cc`;
|
||||||
|
|
||||||
|
export function sneckGroup(size: number, groupLen: number) {
|
||||||
|
const indexArray = new Array<number>(size).fill(0).map((_, i) => i);
|
||||||
|
const chunckSize = Math.round((size / groupLen));
|
||||||
|
const chunckedGroup = chunk(indexArray, groupLen);
|
||||||
|
const reversedGroup = chunckedGroup.map((e, i) => {
|
||||||
|
if (i % 2 === 0) return e;
|
||||||
|
return e.toReversed();
|
||||||
|
});
|
||||||
|
const newGroups: number[][] = [];
|
||||||
|
for (let groupIndex = 0; groupIndex < groupLen; groupIndex++) {
|
||||||
|
const group: number[] = [];
|
||||||
|
for (let colIndex = 0; colIndex < chunckSize; colIndex++) {
|
||||||
|
const data = reversedGroup[colIndex]?.[groupIndex];
|
||||||
|
group.push(data === undefined ? NaN : data);
|
||||||
|
}
|
||||||
|
newGroups.push(group);
|
||||||
|
}
|
||||||
|
return newGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRoundTable<T>(nameList: T[], round: number) {
|
||||||
|
const list = [...nameList];
|
||||||
|
const half = list.length / 2;
|
||||||
|
if (round > list.length - 1) {
|
||||||
|
const left = [...list].slice(0, half);
|
||||||
|
const right = [...list].slice(half);
|
||||||
|
return [left, right];
|
||||||
|
}
|
||||||
|
const sliceStart = (list.length) - round;
|
||||||
|
const slice = list.slice(sliceStart);
|
||||||
|
// console.debug(JSON.stringify({ list }));
|
||||||
|
list.splice(sliceStart);
|
||||||
|
const [first, ...others] = list;
|
||||||
|
const newList = [first, ...slice, ...others].filter(Boolean);
|
||||||
|
// console.debug(JSON.stringify({ sliceStart, len: list.length, first, others, slice, newList }));
|
||||||
|
const left = [...newList].slice(0, half);
|
||||||
|
const right = [...newList].slice(half).reverse();
|
||||||
|
return [left, right];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGameID(matchId: string, user1: string, user2: string) {
|
||||||
|
return [matchId, user1, user2].join('-');
|
||||||
|
}
|
||||||
@ -1,3 +1,98 @@
|
|||||||
|
import type { IEventInfo, Player } from "../types";
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
import { XCXAPI } from "../services/xcxApi";
|
import { XCXAPI } from "../services/xcxApi";
|
||||||
|
import { BASE_URL } from "./common";
|
||||||
|
|
||||||
export const xcxApi = new XCXAPI(process.env.KAIQIUCC_TOKEN ?? '');
|
export const xcxApi = new XCXAPI(process.env.KAIQIUCC_TOKEN ?? '');
|
||||||
|
|
||||||
|
const htmlRequestHeaders = {
|
||||||
|
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||||
|
"accept-language": "zh-CN,zh;q=0.8",
|
||||||
|
"cache-control": "max-age=0",
|
||||||
|
"priority": "u=0, i",
|
||||||
|
"sec-ch-ua": "\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Brave\";v=\"144\"",
|
||||||
|
"sec-ch-ua-mobile": "?0",
|
||||||
|
"sec-ch-ua-platform": "\"Windows\"",
|
||||||
|
"sec-fetch-dest": "document",
|
||||||
|
"sec-fetch-mode": "navigate",
|
||||||
|
"sec-fetch-site": "none",
|
||||||
|
"sec-fetch-user": "?1",
|
||||||
|
"sec-gpc": "1",
|
||||||
|
"upgrade-insecure-requests": "1",
|
||||||
|
"cookie": "SECKEY_ABVK=oTGgqH4ypGPFVdQ3J9K7PoAOPdZ+8R7CsUzi75gelcg%3D; uchome_sendmail=1"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tagid 俱乐部 ID
|
||||||
|
*/
|
||||||
|
export async function listEvent(tagid: string): Promise<IEventInfo[]> {
|
||||||
|
return parseEventList(await fetchEventListHTML(tagid));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param tagid 俱乐部 ID
|
||||||
|
* @return HTML
|
||||||
|
*/
|
||||||
|
export async function fetchEventListHTML(tagid: string) {
|
||||||
|
const url = `${BASE_URL}/home/space.php?do=mtag&tagid=${tagid}&view=event`;
|
||||||
|
const resp = await fetch(url, { headers: htmlRequestHeaders });
|
||||||
|
return resp.text() ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseEventList(html: string) {
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const blockList = $('div.event_list > ol > li');
|
||||||
|
const list: IEventInfo[] = [];
|
||||||
|
for (const block of blockList) {
|
||||||
|
const titleEl = $(block).find('.event_title');
|
||||||
|
const title = titleEl.text();
|
||||||
|
const url = $(titleEl).find('a').attr('href') ?? '';
|
||||||
|
const startDate = $(block).find('ul li:nth-of-type(1)').text().replace('比赛开始: \\t', '').trim();
|
||||||
|
const place = $(block).find('ul li:nth-of-type(2)').text().replace('比赛地点: \\t', '').trim();
|
||||||
|
const event: IEventInfo = {
|
||||||
|
title,
|
||||||
|
info: [startDate, place],
|
||||||
|
url: `${BASE_URL}/home/${url}`,
|
||||||
|
matchId: /\S+-(\d+).html$/.exec(url)?.[1] ?? '',
|
||||||
|
}
|
||||||
|
list.push(event);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param matchId 比赛 ID
|
||||||
|
* @returns HTML
|
||||||
|
*/
|
||||||
|
export async function fetchEventContentHTML(matchId: string) {
|
||||||
|
const url = `${BASE_URL}/home/space.php?do=event&id=${matchId}&view=member&status=2`;
|
||||||
|
const resp = await fetch(url, { headers: htmlRequestHeaders });
|
||||||
|
return resp.text() ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseEventInfo(html: string) {
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
const title = $('h2.title a').text();
|
||||||
|
const itemHref = $('.sub_menu a.active').attr('href') ?? '';
|
||||||
|
const itemId = /\S+item_id=(\d+)$/.exec(itemHref)?.[1] ?? '';
|
||||||
|
const players: Player[] = [];
|
||||||
|
const playersEl = $('.thumb');
|
||||||
|
for (const player of playersEl) {
|
||||||
|
const img = $(player).find('.image img').attr('src') ?? '';
|
||||||
|
const name = $(player).find('h6').text().trim();
|
||||||
|
const uid = /space-(?<uid>\d+).html/.exec($(player).find('h6 a').attr('href') ?? '')?.groups?.uid ?? '';
|
||||||
|
const info = $(player).find('p:nth-of-type(2)').text().replace(/\s/g, '');
|
||||||
|
const score = /^.*?\b(\d+)\b/.exec(info)?.[1] ?? '';
|
||||||
|
players.push({ name, avatar: img, score, info, uid });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
itemId,
|
||||||
|
title,
|
||||||
|
players,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMatchInfo(matchId: string) {
|
||||||
|
return parseEventInfo(await fetchEventContentHTML(matchId));
|
||||||
|
}
|
||||||
|
|||||||
@ -1,141 +0,0 @@
|
|||||||
import type { IEventInfo, Player } from "../types";
|
|
||||||
import * as cheerio from "cheerio";
|
|
||||||
import { chunk } from 'lodash';
|
|
||||||
|
|
||||||
export const BASE_URL = `https://kaiqiuwang.cc`;
|
|
||||||
|
|
||||||
const htmlRequestHeaders = {
|
|
||||||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
||||||
"accept-language": "zh-CN,zh;q=0.8",
|
|
||||||
"cache-control": "max-age=0",
|
|
||||||
"priority": "u=0, i",
|
|
||||||
"sec-ch-ua": "\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Brave\";v=\"144\"",
|
|
||||||
"sec-ch-ua-mobile": "?0",
|
|
||||||
"sec-ch-ua-platform": "\"Windows\"",
|
|
||||||
"sec-fetch-dest": "document",
|
|
||||||
"sec-fetch-mode": "navigate",
|
|
||||||
"sec-fetch-site": "none",
|
|
||||||
"sec-fetch-user": "?1",
|
|
||||||
"sec-gpc": "1",
|
|
||||||
"upgrade-insecure-requests": "1",
|
|
||||||
"cookie": "SECKEY_ABVK=oTGgqH4ypGPFVdQ3J9K7PoAOPdZ+8R7CsUzi75gelcg%3D; uchome_sendmail=1"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param tagid 俱乐部 ID
|
|
||||||
*/
|
|
||||||
export async function listEvent(tagid: string): Promise<IEventInfo[]> {
|
|
||||||
return parseEventList(await fetchEventListHTML(tagid));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param tagid 俱乐部 ID
|
|
||||||
* @return HTML
|
|
||||||
*/
|
|
||||||
export async function fetchEventListHTML(tagid: string) {
|
|
||||||
const url = `${BASE_URL}/home/space.php?do=mtag&tagid=${tagid}&view=event`;
|
|
||||||
const resp = await fetch(url, { headers: htmlRequestHeaders });
|
|
||||||
return resp.text() ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseEventList(html: string) {
|
|
||||||
const $ = cheerio.load(html);
|
|
||||||
const blockList = $('div.event_list > ol > li');
|
|
||||||
const list: IEventInfo[] = [];
|
|
||||||
for (const block of blockList) {
|
|
||||||
const titleEl = $(block).find('.event_title');
|
|
||||||
const title = titleEl.text();
|
|
||||||
const url = $(titleEl).find('a').attr('href') ?? '';
|
|
||||||
const startDate = $(block).find('ul li:nth-of-type(1)').text().replace('比赛开始: \\t', '').trim();
|
|
||||||
const place = $(block).find('ul li:nth-of-type(2)').text().replace('比赛地点: \\t', '').trim();
|
|
||||||
const event: IEventInfo = {
|
|
||||||
title,
|
|
||||||
info: [startDate, place],
|
|
||||||
url: `${BASE_URL}/home/${url}`,
|
|
||||||
matchId: /\S+-(\d+).html$/.exec(url)?.[1] ?? '',
|
|
||||||
}
|
|
||||||
list.push(event);
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param matchId 比赛 ID
|
|
||||||
* @returns HTML
|
|
||||||
*/
|
|
||||||
export async function fetchEventContentHTML(matchId: string) {
|
|
||||||
const url = `${BASE_URL}/home/space.php?do=event&id=${matchId}&view=member&status=2`;
|
|
||||||
const resp = await fetch(url, { headers: htmlRequestHeaders });
|
|
||||||
return resp.text() ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseEventInfo(html: string) {
|
|
||||||
const $ = cheerio.load(html);
|
|
||||||
const title = $('h2.title a').text();
|
|
||||||
const itemHref = $('.sub_menu a.active').attr('href') ?? '';
|
|
||||||
const itemId = /\S+item_id=(\d+)$/.exec(itemHref)?.[1] ?? '';
|
|
||||||
const players: Player[] = [];
|
|
||||||
const playersEl = $('.thumb');
|
|
||||||
for (const player of playersEl) {
|
|
||||||
const img = $(player).find('.image img').attr('src') ?? '';
|
|
||||||
const name = $(player).find('h6').text().trim();
|
|
||||||
const uid = /space-(?<uid>\d+).html/.exec($(player).find('h6 a').attr('href') ?? '')?.groups?.uid ?? '';
|
|
||||||
const info = $(player).find('p:nth-of-type(2)').text().replace(/\s/g, '');
|
|
||||||
const score = /^.*?\b(\d+)\b/.exec(info)?.[1] ?? '';
|
|
||||||
players.push({ name, avatar: img, score, info, uid });
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
itemId,
|
|
||||||
title,
|
|
||||||
players,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getMatchInfo(matchId: string) {
|
|
||||||
return parseEventInfo(await fetchEventContentHTML(matchId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sneckGroup(size: number, groupLen: number) {
|
|
||||||
const indexArray = new Array<number>(size).fill(0).map((_, i) => i);
|
|
||||||
const chunckSize = Math.round((size / groupLen));
|
|
||||||
const chunckedGroup = chunk(indexArray, groupLen);
|
|
||||||
const reversedGroup = chunckedGroup.map((e, i) => {
|
|
||||||
if (i % 2 === 0) return e;
|
|
||||||
return e.toReversed();
|
|
||||||
});
|
|
||||||
const newGroups: number[][] = [];
|
|
||||||
for (let groupIndex = 0; groupIndex < groupLen; groupIndex++) {
|
|
||||||
const group: number[] = [];
|
|
||||||
for (let colIndex = 0; colIndex < chunckSize; colIndex++) {
|
|
||||||
const data = reversedGroup[colIndex]?.[groupIndex];
|
|
||||||
group.push(data === undefined ? NaN : data);
|
|
||||||
}
|
|
||||||
newGroups.push(group);
|
|
||||||
}
|
|
||||||
return newGroups;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRoundTable<T>(nameList: T[], round: number) {
|
|
||||||
const list = [...nameList];
|
|
||||||
const half = list.length / 2;
|
|
||||||
if (round > list.length - 1) {
|
|
||||||
const left = [...list].slice(0, half);
|
|
||||||
const right = [...list].slice(half);
|
|
||||||
return [left, right];
|
|
||||||
}
|
|
||||||
const sliceStart = (list.length) - round;
|
|
||||||
const slice = list.slice(sliceStart);
|
|
||||||
// console.debug(JSON.stringify({ list }));
|
|
||||||
list.splice(sliceStart);
|
|
||||||
const [first, ...others] = list;
|
|
||||||
const newList = [first, ...slice, ...others].filter(Boolean);
|
|
||||||
// console.debug(JSON.stringify({ sliceStart, len: list.length, first, others, slice, newList }));
|
|
||||||
const left = [...newList].slice(0, half);
|
|
||||||
const right = [...newList].slice(half).reverse();
|
|
||||||
return [left, right];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createGameID(matchId: string, user1: string, user2: string) {
|
|
||||||
return [matchId, user1, user2].join('-');
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user