146 lines
6.0 KiB
TypeScript
146 lines
6.0 KiB
TypeScript
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 = /-(?<id>\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} = /比赛开始:.*?(?<y>\d{4})年(?<M>\d{2})月(?<D>\d{2})日 \w+ (?<H>\d{2}):(?<m>\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(),
|
|
}
|
|
}
|
|
|
|
} |