diff --git a/__test__/utils.load-html.test.ts b/__test__/utils.load-html.test.ts index 6531a4e..3f0309f 100644 --- a/__test__/utils.load-html.test.ts +++ b/__test__/utils.load-html.test.ts @@ -1,7 +1,7 @@ import { expect, test } from 'bun:test'; import path from 'path'; import fs from 'fs'; -import { fetchEventContentHTML, fetchEventListHTML } from '../src/utils/utils'; +import { fetchEventContentHTML, fetchEventListHTML } from '../src/utils/'; test('load html', async () => { const saveTo = path.resolve(__dirname, 'data', 'view-event.html'); diff --git a/__test__/utils.test.ts b/__test__/utils.test.ts index 1fe7d25..c9a40d0 100644 --- a/__test__/utils.test.ts +++ b/__test__/utils.test.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import { expect, test } from 'bun:test'; import path from 'path'; -import { parseEventInfo, parseEventList, sneckGroup } from '../src/utils/utils'; +import { parseEventInfo, parseEventList, sneckGroup } from '../src/utils/'; const matchId = '167684'; diff --git a/src/components/GroupMember.tsx b/src/components/GroupMember.tsx index 4860ae8..e9fc84b 100644 --- a/src/components/GroupMember.tsx +++ b/src/components/GroupMember.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from "react"; import { Button, Card, Divider, Drawer, Flex, Space, Table } from "antd"; import type { BasePlayer } from "../types"; -import { getRoundTable } from "../utils/utils"; +import { getRoundTable } from "../utils/common"; import User from "./User"; interface Props { diff --git a/src/components/GroupingPrediction.tsx b/src/components/GroupingPrediction.tsx index 0c51aac..47aef94 100644 --- a/src/components/GroupingPrediction.tsx +++ b/src/components/GroupingPrediction.tsx @@ -3,7 +3,7 @@ import { Flex, Form, InputNumber, Segmented, Switch } from "antd"; import { chunk } from 'lodash'; import type { BasePlayer } from "../types"; import { GroupMember } from "./GroupMember"; -import { sneckGroup } from "../utils/utils"; +import { sneckGroup } from "../utils/common"; interface Player extends BasePlayer { nowScore?: string; diff --git a/src/index.tsx b/src/index.tsx index 1352a3f..5b04488 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,6 @@ import { serve } from "bun"; -import { getMatchInfo, listEvent } from "./utils/utils"; +import { getMatchInfo, listEvent, xcxApi } from "./utils/server"; import index from "./index.html"; -import { xcxApi } from "./utils/server"; import { getUidScore } from "./services/uidScoreStore"; if (!process.env.KAIQIUCC_TOKEN) { diff --git a/src/services/xcxApi.ts b/src/services/xcxApi.ts index 7e35117..0fedf11 100644 --- a/src/services/xcxApi.ts +++ b/src/services/xcxApi.ts @@ -1,5 +1,5 @@ -import type { GamesData, XCXFindUser, XCXFindUserResp, XCXMember, XCXProfile, XCXTag } from "../types"; -import { BASE_URL } from "../utils/utils"; +import type { GamesData, XCXFindUserResp, XCXMember, XCXProfile, XCXTag } from "../types"; +import { BASE_URL } from "../utils/common"; const XCX_BASE_URL = `${BASE_URL}/xcx/public/index.php`; diff --git a/src/utils/common.ts b/src/utils/common.ts new file mode 100644 index 0000000..5ebae3a --- /dev/null +++ b/src/utils/common.ts @@ -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(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(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('-'); +} \ No newline at end of file diff --git a/src/utils/server.ts b/src/utils/server.ts index e0cff57..d87471c 100644 --- a/src/utils/server.ts +++ b/src/utils/server.ts @@ -1,3 +1,98 @@ +import type { IEventInfo, Player } from "../types"; +import * as cheerio from "cheerio"; import { XCXAPI } from "../services/xcxApi"; +import { BASE_URL } from "./common"; -export const xcxApi = new XCXAPI(process.env.KAIQIUCC_TOKEN ?? ''); \ No newline at end of file +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 { + 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-(?\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)); +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts deleted file mode 100644 index c529a25..0000000 --- a/src/utils/utils.ts +++ /dev/null @@ -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 { - 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-(?\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(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(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('-'); -}