diff options
| author | Fuwn <[email protected]> | 2024-02-07 02:07:54 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-02-07 02:07:54 -0800 |
| commit | 25b78c025491a00379fd7f79aa84c1cdd81fedf0 (patch) | |
| tree | e08a3097689d4e880d375fa4d86e991781de2c19 /src/routes | |
| parent | feat(hololive): temporarily remove (diff) | |
| download | due.moe-25b78c025491a00379fd7f79aa84c1cdd81fedf0.tar.xz due.moe-25b78c025491a00379fd7f79aa84c1cdd81fedf0.zip | |
refactor(hololive): move to client-side evaluation
Diffstat (limited to 'src/routes')
| -rw-r--r-- | src/routes/+layout.svelte | 2 | ||||
| -rw-r--r-- | src/routes/api/hololive/+server.ts.bak | 182 | ||||
| -rw-r--r-- | src/routes/hololive/+page.svelte (renamed from src/routes/hololive/+page.svelte.bak) | 15 |
3 files changed, 12 insertions, 187 deletions
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index ce58e600..cc09bb94 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -113,7 +113,7 @@ <Dropdown items={[ { name: $locale().navigation.subtitleSchedule, url: root('/schedule') }, - // { name: $locale().navigation.hololive, url: root('/hololive') }, + { name: $locale().navigation.hololive, url: root('/hololive') }, { name: $locale().tools.tool.characterBirthdays.short, url: root('/birthdays') }, { name: $locale().navigation.newReleases, url: root('/updates') } ]} diff --git a/src/routes/api/hololive/+server.ts.bak b/src/routes/api/hololive/+server.ts.bak deleted file mode 100644 index fae1482b..00000000 --- a/src/routes/api/hololive/+server.ts.bak +++ /dev/null @@ -1,182 +0,0 @@ -import { JSDOM } from 'jsdom'; - -export const GET = async ({ url }) => - Response.json( - parseScheduleHtml( - await ( - await fetch('https://schedule.hololive.tv', { - headers: { - Cookie: `timezone=${url.searchParams.get('timezone') || 'Asia/Tokyo'}` - } - }) - ).text() - ), - { - headers: { - 'Access-Control-Allow-Origin': 'https://due.moe', - 'Cache-Control': 'public, max-age=300, s-maxage=300' - } - } - ); - -// 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 | Buffer): LiveBlock[] { - const { window } = new JSDOM(html); - const { document } = window; - const year = new Date().getFullYear().toString(); - - const rows = document.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: NodeListOf<HTMLAnchorElement> = row.querySelectorAll('a.thumbnail'); - allThumbnail.forEach((thumbnail) => { - const link = thumbnail.href; - const streaming = thumbnail.style.borderColor === 'red'; - 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 - */ -function parseScheduleHtml(html: string | Buffer, 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 }; -} diff --git a/src/routes/hololive/+page.svelte.bak b/src/routes/hololive/+page.svelte index 76c0707f..42e01075 100644 --- a/src/routes/hololive/+page.svelte.bak +++ b/src/routes/hololive/+page.svelte @@ -3,6 +3,7 @@ import Message from '$lib/Loading/Message.svelte'; import Skeleton from '$lib/Loading/Skeleton.svelte'; import HeadTitle from '$lib/Home/HeadTitle.svelte'; + import { parseScheduleHtml } from '$lib/hololive'; interface ParseResult { lives: { @@ -19,7 +20,13 @@ let schedulePromise: Promise<Response>; - onMount(() => (schedulePromise = fetch('/api/hololive'))); + onMount(async () => { + schedulePromise = fetch('https://schedule.hololive.tv', { + headers: { + Cookie: 'timezone=Asia/Tokyo' + } + }); + }); const typeSchedule = (schedule: any) => schedule as ParseResult; </script> @@ -32,15 +39,15 @@ <Skeleton grid={true} count={100} width="49%" height="16.25em" /> {:then scheduleResponse} {#if scheduleResponse} - {#await scheduleResponse.json()} + {#await scheduleResponse.text()} <Message message="Parsing schedule ..." /> <Skeleton grid={true} count={100} width="49%" height="16.25em" /> {:then untypedSchedule} - {@const schedule = typeSchedule(untypedSchedule)} + {@const schedule = typeSchedule(parseScheduleHtml(untypedSchedule))} {#if schedule.lives.length === 0} - <Message message="No upcoming streams." /> + <Message message="No upcoming streams." loader="ripple" /> {/if} <div class="container"> |