aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2024-02-07 01:07:34 -0800
committerFuwn <[email protected]>2024-02-07 01:07:34 -0800
commitb2cd85ceddb1d39924b858cb198a9784954fd4c5 (patch)
tree62c7afc68bbdf06da7221d0763db0c03bc28fcfa /src
parentfix(match): skip specials and ovas in schedule (diff)
downloaddue.moe-b2cd85ceddb1d39924b858cb198a9784954fd4c5.tar.xz
due.moe-b2cd85ceddb1d39924b858cb198a9784954fd4c5.zip
feat(hololive): hololive schedule
Diffstat (limited to 'src')
-rw-r--r--src/app.html2
-rw-r--r--src/lib/Loading/Message.svelte2
-rw-r--r--src/lib/Locale/english.ts3
-rw-r--r--src/lib/Locale/japanese.ts3
-rw-r--r--src/lib/Locale/layout.ts1
-rw-r--r--src/routes/+layout.svelte5
-rw-r--r--src/routes/api/hololive/+server.ts10
-rw-r--r--src/routes/hololive/+page.svelte127
8 files changed, 148 insertions, 5 deletions
diff --git a/src/app.html b/src/app.html
index 293aa41b..5a221a19 100644
--- a/src/app.html
+++ b/src/app.html
@@ -79,6 +79,8 @@
></script>
<script>
+ var global = global || window;
+
aoButa = '';
document.addEventListener('DOMContentLoaded', () => {
diff --git a/src/lib/Loading/Message.svelte b/src/lib/Loading/Message.svelte
index 758c4039..247ad3b3 100644
--- a/src/lib/Loading/Message.svelte
+++ b/src/lib/Loading/Message.svelte
@@ -20,7 +20,7 @@
{/if}
{#if message}
- <p />
+ <br />
{message}
{/if}
diff --git a/src/lib/Locale/english.ts b/src/lib/Locale/english.ts
index fe449d34..5477632c 100644
--- a/src/lib/Locale/english.ts
+++ b/src/lib/Locale/english.ts
@@ -11,7 +11,8 @@ const English: Locale = {
profile: 'Profile',
logIn: 'Log in with AniList',
logOut: 'Log out',
- schedule: 'Schedule'
+ schedule: 'Schedule',
+ hololive: 'hololive'
},
settings: {
fields: {
diff --git a/src/lib/Locale/japanese.ts b/src/lib/Locale/japanese.ts
index 7326a3a0..09b7dc32 100644
--- a/src/lib/Locale/japanese.ts
+++ b/src/lib/Locale/japanese.ts
@@ -11,7 +11,8 @@ const Japanese: Locale = {
profile: 'プロフィール',
logIn: 'AniListでログインする',
logOut: 'ログアウト',
- schedule: 'スケジュール'
+ schedule: 'スケジュール',
+ hololive: 'ホロライブ'
},
settings: {
fields: {
diff --git a/src/lib/Locale/layout.ts b/src/lib/Locale/layout.ts
index d2af0a4c..e1c81e0f 100644
--- a/src/lib/Locale/layout.ts
+++ b/src/lib/Locale/layout.ts
@@ -16,6 +16,7 @@ export interface Locale {
logIn: LocaleValue;
logOut: LocaleValue;
schedule: LocaleValue;
+ hololive: LocaleValue;
};
settings: {
fields: {
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index e8fb9f9b..cc09bb94 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -113,8 +113,9 @@
<Dropdown
items={[
{ name: $locale().navigation.subtitleSchedule, url: root('/schedule') },
- { name: $locale().navigation.newReleases, url: root('/updates') },
- { name: $locale().tools.tool.characterBirthdays.short, url: root('/birthdays') }
+ { name: $locale().navigation.hololive, url: root('/hololive') },
+ { name: $locale().tools.tool.characterBirthdays.short, url: root('/birthdays') },
+ { name: $locale().navigation.newReleases, url: root('/updates') }
]}
title={$locale().navigation.schedule}
/>
diff --git a/src/routes/api/hololive/+server.ts b/src/routes/api/hololive/+server.ts
new file mode 100644
index 00000000..331d776a
--- /dev/null
+++ b/src/routes/api/hololive/+server.ts
@@ -0,0 +1,10 @@
+import parseScheduleHtml from 'holo-schedule';
+import getScheduleHtml from 'holo-schedule/lib/getScheduleHtml';
+
+export const GET = async () =>
+ Response.json(parseScheduleHtml(await getScheduleHtml()), {
+ headers: {
+ 'Access-Control-Allow-Origin': 'https://due.moe',
+ 'Cache-Control': 'public, max-age=300, s-maxage=300'
+ }
+ });
diff --git a/src/routes/hololive/+page.svelte b/src/routes/hololive/+page.svelte
new file mode 100644
index 00000000..bd49bbf8
--- /dev/null
+++ b/src/routes/hololive/+page.svelte
@@ -0,0 +1,127 @@
+<script lang="ts">
+ import { onMount } from 'svelte';
+ import Message from '$lib/Loading/Message.svelte';
+ import Skeleton from '$lib/Loading/Skeleton.svelte';
+
+ interface ParseResult {
+ lives: {
+ time: Date;
+ link: string;
+ videoId: string;
+ streamer: string;
+ livePreviewImage: string;
+ guests: string[];
+ streaming: boolean;
+ }[];
+ dict: Record<string, string>;
+ }
+
+ let schedulePromise: Promise<Response>;
+
+ onMount(() => (schedulePromise = fetch('/api/hololive')));
+
+ const typeSchedule = (schedule: any) => schedule as ParseResult;
+</script>
+
+{#await schedulePromise}
+ <Message message="Loading schedule ..." />
+
+ <Skeleton grid={true} count={100} width="49%" height="16.25em" />
+{:then scheduleResponse}
+ {#if scheduleResponse}
+ {#await scheduleResponse.json()}
+ <Message message="Parsing schedule ..." />
+
+ <Skeleton grid={true} count={100} width="49%" height="16.25em" />
+ {:then untypedSchedule}
+ {@const schedule = typeSchedule(untypedSchedule)}
+
+ {#if schedule.lives.length === 0}
+ <Message message="No upcoming streams." />
+ {/if}
+
+ <div class="container">
+ {#each schedule.lives
+ .filter((live) => {
+ const time = new Date(live.time);
+
+ return time.getTime() > Date.now() - 12 * 60 * 60 * 1000 || time.getTime() > Date.now() || live.streaming;
+ })
+ .sort((a, b) => {
+ if (a.streaming && !b.streaming) return -1;
+
+ if (!a.streaming && b.streaming) return 1;
+
+ return new Date(a.time).getTime() - new Date(b.time).getTime();
+ }) as live}
+ <div class="stream card">
+ <p>
+ [{#if live.streaming}
+ <b class="live">LIVE</b>{:else}
+ <span class="upcoming">Upcoming</span>{/if}]
+ <b>{live.streamer}</b> <span class="opaque">|</span>
+ {new Date(live.time).toLocaleString()}
+ {#if live.guests.length > 0}
+ <br />
+ <small>
+ With {live.guests.join(', ').replace(/, ([^,]+)$/, ', & $1')}
+ </small>
+ {/if}
+ </p>
+
+ <a href={live.link} class="preview">
+ <img src={live.livePreviewImage} alt="Stream Preview" />
+ </a>
+ </div>
+ {/each}
+ </div>
+ {:catch}
+ <Message message="Could not parse schedule. Please try again later." loader="ripple" />
+ {/await}
+ {:else}
+ <Message message="Loading schedule ..." />
+
+ <Skeleton grid={true} count={100} width="49%" height="16.25em" />
+ {/if}
+{:catch}
+ <Message message="Could not load schedule. Please try again later." loader="ripple" />
+{/await}
+
+<style lang="scss">
+ .preview {
+ // margin: 0.15rem;
+
+ img {
+ border-radius: 8px;
+ height: 20vh;
+ transition: transform 0.3s ease;
+ }
+
+ &:hover {
+ img {
+ position: relative;
+ z-index: 2;
+ transition: transform 0.3s ease;
+ transform: scale(1.05);
+ }
+ }
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .live {
+ color: var(--base0F);
+ }
+
+ .upcoming {
+ color: var(--base0C);
+ }
+
+ .container {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(22.5em, 1fr));
+ gap: 0.5em;
+ }
+</style>