import * as cheerio from "cheerio"; import { htmlRequestHeaders, redis, REDIS_CACHE_HOUR } from "../utils/server"; import type { ClubInfo, IEventInfo } from "../types"; import dayjs from "dayjs"; export class KaiqiuService { static #baseURL = 'https://kaiqiuwang.cc'; public static async findClub(name: string, page = 1, normalClub?: boolean) { const searchKey = encodeURIComponent(name); const url = `${this.#baseURL}/home/space.php?province=&city=&searchkey=${searchKey}&searchsubmit=%E6%90%9C%E7%B4%A2&searchmode=1&do=mtag&view=hot&page=${page}`; const html = await fetch(url, { headers: htmlRequestHeaders, }).then(res => res.text()); return this.parseSearchClubPage(html, Boolean(normalClub)); } private static parseSearchClubPage(html: string, normal: boolean): { clubs: ClubInfo[]; total: number; } { const $ = cheerio.load(html); const infos = $(`#content > div:nth-of-type(${normal ? 2 : 1}) td[width="250"]`).toArray(); const imgs = $(`#content > div:nth-of-type(${normal ? 2 : 1}) td[width="80"]`).toArray(); const parseInfo = (info: typeof infos[number], index: number): ClubInfo => { const name = $(info).find('a').text().trim(); const url = $(info).find('a').attr('href') ?? ''; const clubId = /-(?\d+).html$/.exec(url)?.groups?.id?.toString() ?? ''; const members = parseInt($(info).find('span.num').text().trim()); const area = $(info).find('div.gray:nth-of-type(1)').text().trim(); const src = $(imgs[index]).find('.threadimg60 img').attr('src') ?? ''; const img = src.startsWith('http') ? src : 'https://kaiqiuwang.cc/home/image/nologo.jpg'; return { name, id: clubId, members, area, img, url: `${this.#baseURL}/home/${url}` }; } const clubs = infos.map((e, i) => parseInfo(e, i)); const total = parseInt($('#content > div:nth-of-type(1) div.page > em').text().trim()) || clubs.length; return { clubs, total }; } public static async getClubInfo(clubId: string) { if (!clubId) return null; const url = `${this.#baseURL}/home/space-mtag-tagid-${clubId}.html`; const html = await fetch(url, { headers: htmlRequestHeaders }).then(res => res.text()); const $ = cheerio.load(html); const name = $('h2.title a:nth-of-type(2)').text().trim(); const src = $('#space_avatar img').attr('src') ?? ''; const img = src.startsWith('http') ? src : 'https://kaiqiuwang.cc/home/image/nologo.jpg'; const article = $('.article').text().trim(); return { id: clubId, name, img, article }; } static async #fetchEventListHTML(clubId: string, page: number) { const url = `https://kaiqiuwang.cc/home/space-0-do-mtag-tagid-${clubId}-view-event-page-${page}.html`; const resp = await fetch(url, { headers: htmlRequestHeaders }); return resp.text(); } public static async listClubEvents(clubId: string, page = 1) { if (!clubId) return { data: [], total: 0, location: { lat: 0, lng: 0, }, }; const key = `my-kaiqiuwang:club-events:${clubId}:page-${page}`; let html = await redis.get(key).catch(() => ''); if (!html) { html = await this.#fetchEventListHTML(clubId, page); redis.setex(key, 60 * 60 * REDIS_CACHE_HOUR, html); } const location = await this.#getClubLocation(clubId); const data = await this.#parseEventList(html); return { ...data, location, } } static async #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 place = $(block).find('ul li:nth-of-type(2)').text().replace('比赛地点: \\t', '').trim(); const eventPath = $(titleEl).find('a').attr('href') ?? ''; const eventURL = `${this.#baseURL}/home/${eventPath}`; const matchId = /\S+-(\d+).html$/.exec(eventPath)?.[1] ?? ''; const { startDate } = await this.getEventInfo(matchId); const event: IEventInfo = { title, info: [`比赛时间:${startDate}`, place], url: eventURL, startDate, matchId, isFinished: dayjs(startDate).isBefore(), } list.push(event); } const total = parseInt($('#mainarea > div.event_list > div > em').text().trim()); return { data: list, total: total || list.length, }; } static async #getClubLocation(clubId: string) { const url = `${this.#baseURL}/home/space-mtag-tagid-${clubId}-view-map.html`; const key = `my-kaiqiuwang:location:${clubId}`; let html: string = await redis.get(key) || ''; if (!html) { html = await fetch(url, { headers: htmlRequestHeaders }).then(res => res.text()); redis.setex(key, 60 * 60 * 24, html); } const $ = cheerio.load(html); const lng = Number($('#lng').val()); const lat = Number($('#lat').val()); return { lng, lat }; } public static async getEventInfo(eventId: string) { let eventPage = await redis.get(`my-kaiqiuwang:match:${eventId}`) ?? ''; // https://kaiqiuwang.cc/home/space-event-id-175775.html const eventURL = `${this.#baseURL}/home/space-event-id-${eventId}.html`; if (!eventPage) { eventPage = await fetch(eventURL, { headers: htmlRequestHeaders }).then(res => res.text() ?? ''); await redis.setex(`my-kaiqiuwang:match:${eventId}`, 60 * 60 * 10, eventPage) } const $ = cheerio.load(eventPage); const eventContent = $('.event_content').text().replace(/(\r|\n)/g, ',').split(',').filter(Boolean).join(' '); const { y, M, D, H, m} = /比赛开始:.*?(?\d{4})年(?\d{2})月(?\d{2})日 \w+ (?\d{2}):(?\d{2})/ .exec(eventContent)?.groups ?? {}; const startDate = y ? `${y}-${M}-${D} ${H}:${m}` : ''; const title = $('#mainarea > h2 > a:nth-child(3)').text().trim(); return { title, startDate, url: eventURL, isFinished: dayjs(startDate).isBefore(), } } }