aboutsummaryrefslogtreecommitdiff
path: root/src/lib/Data
diff options
context:
space:
mode:
authorFuwn <[email protected]>2024-02-08 00:02:55 -0800
committerFuwn <[email protected]>2024-02-08 00:02:55 -0800
commite664f182606b197e72fbb6c9729a540ee2226068 (patch)
tree619e07b9ce85d67dbd92ee7e98dcf2fb87f7b9a9 /src/lib/Data
parentrefactor(birthdays): move to data module (diff)
downloaddue.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.ts162
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 };
+}