aboutsummaryrefslogtreecommitdiff
path: root/src/routes
diff options
context:
space:
mode:
authorFuwn <[email protected]>2024-02-07 02:07:54 -0800
committerFuwn <[email protected]>2024-02-07 02:07:54 -0800
commit25b78c025491a00379fd7f79aa84c1cdd81fedf0 (patch)
treee08a3097689d4e880d375fa4d86e991781de2c19 /src/routes
parentfeat(hololive): temporarily remove (diff)
downloaddue.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.svelte2
-rw-r--r--src/routes/api/hololive/+server.ts.bak182
-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">