diff options
| author | Fuwn <[email protected]> | 2024-02-08 00:02:55 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-02-08 00:02:55 -0800 |
| commit | e664f182606b197e72fbb6c9729a540ee2226068 (patch) | |
| tree | 619e07b9ce85d67dbd92ee7e98dcf2fb87f7b9a9 /src/lib/Data | |
| parent | refactor(birthdays): move to data module (diff) | |
| download | due.moe-e664f182606b197e72fbb6c9729a540ee2226068.tar.xz due.moe-e664f182606b197e72fbb6c9729a540ee2226068.zip | |
refactor(hololive): move to data module
Diffstat (limited to 'src/lib/Data')
| -rw-r--r-- | src/lib/Data/hololive.ts | 162 |
1 files changed, 162 insertions, 0 deletions
diff --git a/src/lib/Data/hololive.ts b/src/lib/Data/hololive.ts new file mode 100644 index 00000000..d389cff2 --- /dev/null +++ b/src/lib/Data/hololive.ts @@ -0,0 +1,162 @@ +// https://github.com/wabilin/holo-schedule + +function mapNodeList<E extends Element, T>(list: NodeListOf<E>, mapper: (ele: E) => T): T[] { + const ary: T[] = []; + list.forEach((node) => { + ary.push(mapper(node)); + }); + + return ary; +} + +function selectTrimTextContent(ele: Element, selector: string): string { + return ele.querySelector(selector)?.textContent?.trim() || ''; +} + +const livePreviewImageHosts: readonly string[] = [ + 'img.youtube.com', + 'schedule-static.hololive.tv' +] as const; + +function hostOf(src: string) { + return new URL(src).host; +} + +function dataFromAThumbnail(thumb: Element) { + const time = selectTrimTextContent(thumb, '.datetime'); + const name = selectTrimTextContent(thumb, '.name'); + + const images = mapNodeList(thumb.querySelectorAll('img'), (img) => img.src); + + const avatarImages = images.filter((src) => hostOf(src) === 'yt3.ggpht.com'); + const livePreviewImage = images.find((src) => livePreviewImageHosts.includes(hostOf(src))) || ''; + + return { + time, + name, + avatarImages, + livePreviewImage + }; +} + +interface LiveBlock { + time: Date; + streamer: string; + avatarImages: string[]; + livePreviewImage: string; + link: string; + streaming: boolean; +} + +function parseToLiveBlocks(html: string): LiveBlock[] { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const year = new Date().getFullYear().toString(); + + const rows = doc.querySelectorAll('#all > .container > .row'); + + let date = ''; + const lives: LiveBlock[] = []; + + rows.forEach((row) => { + const dateDiv = row.querySelector('.holodule'); + if (dateDiv) { + date = dateDiv.textContent?.replace(/\s+/g, '') || ''; + date = date.match(/\d+\/\d+/)![0].replace('/', '-'); + } + + const allThumbnail = row.querySelectorAll('a.thumbnail'); + allThumbnail.forEach((t) => { + const thumbnail = t as HTMLAnchorElement; + const link = thumbnail.href; + const streaming = thumbnail.style.borderColor === 'red'; + // The dataFromAThumbnail function needs to be adjusted to work with the parsed document. + const { time, name, avatarImages, livePreviewImage } = dataFromAThumbnail(thumbnail); + + lives.push({ + link, + avatarImages, + livePreviewImage, + time: new Date(`${year}-${date}T${time}:00+09:00`), + streamer: name, + streaming + }); + }); + }); + + return lives; +} + +export type StreamerImageDict = Record<string, string>; +type ImageStreamerDict = Record<string, string>; + +function nextStreamerImageDict(liveBlocks: LiveBlock[], oldDict: StreamerImageDict) { + const dict = { ...oldDict }; + liveBlocks.forEach(({ avatarImages: images, streamer }) => { + dict[streamer] = images[0]; + }); + + return dict; +} + +function reverseDict(dict: StreamerImageDict): ImageStreamerDict { + const reversed: ImageStreamerDict = {}; + Object.entries(dict).forEach(([streamer, img]) => { + reversed[img] = streamer; + }); + + return reversed; +} + +export interface LiveInfo { + time: Date; + link: string; + videoId: string; + streamer: string; + livePreviewImage: string; + guests: string[]; + streaming: boolean; +} + +interface ParseResult { + lives: LiveInfo[]; + dict: StreamerImageDict; +} + +function getVideoId(link: string): string { + return link.replace('https://www.youtube.com/watch?v=', ''); +} + +/** + * @param html - Html of https://schedule.hololive.tv. Get with Japan timezone (GTM+9) + * @param storedDict - An object stored { vtuberName: iconImageSrc } + * @returns - Lives schedule and updated dict + */ +export function parseScheduleHtml(html: string, storedDict: StreamerImageDict = {}): ParseResult { + const liveBlocks = parseToLiveBlocks(html); + const streamerImageDict = nextStreamerImageDict(liveBlocks, storedDict); + + const dict = reverseDict(streamerImageDict); + + const lives = liveBlocks.map((liveBlocks) => { + const { streamer, avatarImages, time, link, livePreviewImage, streaming } = liveBlocks; + + const guests = avatarImages + .splice(1) + .map((x) => dict[x]) + .filter(Boolean); + const videoId = getVideoId(link); + + return { + time, + streamer, + guests, + link, + videoId, + livePreviewImage, + streaming + }; + }); + + return { lives, dict: streamerImageDict }; +} |