aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2023-12-16 18:50:51 -0800
committerFuwn <[email protected]>2023-12-16 18:50:51 -0800
commitb018bd62213b1a72114ecf47c6676204e33bd429 (patch)
tree8b8ece187917e99fc5591fe2f5fa271861e8b260
parentfeat(api): subsplease endpoint (diff)
downloaddue.moe-b018bd62213b1a72114ecf47c6676204e33bd429.tar.xz
due.moe-b018bd62213b1a72114ecf47c6676204e33bd429.zip
feat(anime): subtitle countdown
-rwxr-xr-xbun.lockbbin124086 -> 125230 bytes
-rw-r--r--package.json90
-rw-r--r--src/lib/AniList/media.ts3
-rw-r--r--src/lib/List/Anime/CleanAnimeList.svelte67
-rw-r--r--src/lib/Media/anime.ts88
-rw-r--r--src/lib/Tools/SequelSpy.svelte10
-rw-r--r--src/lib/subsPlease.ts11
-rw-r--r--src/routes/api/subsplease/+server.ts7
-rw-r--r--src/routes/settings/+page.svelte6
-rw-r--r--src/stores/settings.ts2
10 files changed, 202 insertions, 82 deletions
diff --git a/bun.lockb b/bun.lockb
index 5dacf3a3..2225371f 100755
--- a/bun.lockb
+++ b/bun.lockb
Binary files differ
diff --git a/package.json b/package.json
index ce3c6f54..1939447d 100644
--- a/package.json
+++ b/package.json
@@ -1,46 +1,48 @@
{
- "name": "due.moe",
- "version": "0.0.0",
- "private": true,
- "scripts": {
- "dev": "vite dev",
- "build": "vite build",
- "preview": "vite preview",
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
- "lint": "prettier --plugin-search-dir . --check . && eslint .",
- "format": "prettier --plugin-search-dir . --write ."
- },
- "devDependencies": {
- "@sveltejs/adapter-auto": "^2.0.0",
- "@sveltejs/kit": "^1.20.4",
- "@types/dom-to-image": "^2.6.5",
- "@types/file-saver": "^2.0.5",
- "@types/jsdom": "^21.1.6",
- "@typescript-eslint/eslint-plugin": "^5.45.0",
- "@typescript-eslint/parser": "^5.45.0",
- "eslint": "^8.28.0",
- "eslint-config-prettier": "^8.5.0",
- "eslint-plugin-svelte": "^2.30.0",
- "prettier": "^2.8.0",
- "prettier-plugin-svelte": "^2.10.1",
- "svelte": "^4.0.5",
- "svelte-adapter-bun": "^0.5.0",
- "svelte-check": "^3.4.3",
- "tslib": "^2.4.1",
- "typescript": "^5.0.0",
- "vite": "^4.4.2"
- },
- "type": "module",
- "dependencies": {
- "bun-types": "^1.0.15",
- "dexie": "^4.0.1-alpha.25",
- "html2canvas": "^1.4.1",
- "jsdom": "^23.0.1",
- "modern-screenshot": "^4.4.33",
- "rss-parser": "^3.13.0",
- "socket.io": "^4.7.2",
- "socket.io-client": "^4.7.2",
- "wanakana": "^5.3.1"
- }
+ "name": "due.moe",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+ "lint": "prettier --plugin-search-dir . --check . && eslint .",
+ "format": "prettier --plugin-search-dir . --write ."
+ },
+ "devDependencies": {
+ "@sveltejs/adapter-auto": "^2.0.0",
+ "@sveltejs/kit": "^1.20.4",
+ "@types/dom-to-image": "^2.6.5",
+ "@types/fast-levenshtein": "^0.0.4",
+ "@types/file-saver": "^2.0.5",
+ "@types/jsdom": "^21.1.6",
+ "@typescript-eslint/eslint-plugin": "^5.45.0",
+ "@typescript-eslint/parser": "^5.45.0",
+ "eslint": "^8.28.0",
+ "eslint-config-prettier": "^8.5.0",
+ "eslint-plugin-svelte": "^2.30.0",
+ "prettier": "^2.8.0",
+ "prettier-plugin-svelte": "^2.10.1",
+ "svelte": "^4.0.5",
+ "svelte-adapter-bun": "^0.5.0",
+ "svelte-check": "^3.4.3",
+ "tslib": "^2.4.1",
+ "typescript": "^5.0.0",
+ "vite": "^4.4.2"
+ },
+ "type": "module",
+ "dependencies": {
+ "bun-types": "^1.0.15",
+ "dexie": "^4.0.1-alpha.25",
+ "fast-levenshtein": "^3.0.0",
+ "html2canvas": "^1.4.1",
+ "jsdom": "^23.0.1",
+ "modern-screenshot": "^4.4.33",
+ "rss-parser": "^3.13.0",
+ "socket.io": "^4.7.2",
+ "socket.io-client": "^4.7.2",
+ "wanakana": "^5.3.1"
+ }
}
diff --git a/src/lib/AniList/media.ts b/src/lib/AniList/media.ts
index ff43732e..3338dd9a 100644
--- a/src/lib/AniList/media.ts
+++ b/src/lib/AniList/media.ts
@@ -41,6 +41,7 @@ export interface Media {
episode: number;
airingAt?: number;
};
+ synonyms: string[];
mediaListEntry?: {
progress: number;
progressVolumes: number;
@@ -105,7 +106,7 @@ const collectionQueryTemplate = (type: Type, userId: number, includeCompleted: b
lists {
name entries {
media {
- id idMal status type episodes chapters format duration
+ id idMal status type episodes chapters format duration synonyms
title { romaji english native }
nextAiringEpisode { episode airingAt }
mediaListEntry {
diff --git a/src/lib/List/Anime/CleanAnimeList.svelte b/src/lib/List/Anime/CleanAnimeList.svelte
index 0b130d15..fd31963c 100644
--- a/src/lib/List/Anime/CleanAnimeList.svelte
+++ b/src/lib/List/Anime/CleanAnimeList.svelte
@@ -8,7 +8,8 @@
import ListTitle from '../ListTitle.svelte';
import MediaTitle from '../MediaTitleDisplay.svelte';
import { outboundLink } from '$lib/Media/media';
- import { onDestroy } from 'svelte';
+ import { onDestroy, onMount } from 'svelte';
+ import type { SubsPlease } from '$lib/subsPlease';
export let media: Media[];
export let title: string;
@@ -73,42 +74,46 @@
{#if title !== 'Upcoming Episodes' || !$settings.displayCountdownRightAligned}
<span style="opacity: 50%;">|</span>
{/if}
- {#if title !== 'Upcoming Episodes'}
- <!-- {anime.mediaListEntry?.progress || 0}{@html totalEpisodes(anime)} -->
- {pendingUpdate === anime.id ? progress + 1 : progress}{@html totalEpisodes(anime)}
- <a
- href={'#'}
- style={pendingUpdate === anime.id ? 'pointer-events: none; opacity: 50%;' : ''}
- on:click={() => {
- if (pendingUpdate !== anime.id) {
- lastUpdatedMedia = anime.id;
- pendingUpdate = anime.id;
+ {#await fetch(`/api/subsplease?tz=${Intl.DateTimeFormat().resolvedOptions().timeZone}`).then( (r) => r.json() )}
+ Loading ...
+ {:then subsPlease}
+ {#if title !== 'Upcoming Episodes'}
+ <!-- {anime.mediaListEntry?.progress || 0}{@html totalEpisodes(anime)} -->
+ {pendingUpdate === anime.id ? progress + 1 : progress}{@html totalEpisodes(anime)}
+ <a
+ href={'#'}
+ style={pendingUpdate === anime.id ? 'pointer-events: none; opacity: 50%;' : ''}
+ on:click={() => {
+ if (pendingUpdate !== anime.id) {
+ lastUpdatedMedia = anime.id;
+ pendingUpdate = anime.id;
- updateMedia(anime.id, anime.mediaListEntry?.progress, () => {
- const mediaListEntry = media.find((m) => m.id === anime.id)?.mediaListEntry;
+ updateMedia(anime.id, anime.mediaListEntry?.progress, () => {
+ const mediaListEntry = media.find((m) => m.id === anime.id)?.mediaListEntry;
- if (mediaListEntry) mediaListEntry.progress = progress + 1;
+ if (mediaListEntry) mediaListEntry.progress = progress + 1;
- previousAnimeList = media;
- animeLists = cleanCache(user, identity);
- pendingUpdate = null;
- });
- }
- }}>+</a
- >
- {#if !completed}
- [{anime.nextAiringEpisode?.episode === -1
- ? '?'
- : (anime.nextAiringEpisode?.episode || 1) - 1}]
+ previousAnimeList = media;
+ animeLists = cleanCache(user, identity);
+ pendingUpdate = null;
+ });
+ }
+ }}>+</a
+ >
+ {#if !completed}
+ [{anime.nextAiringEpisode?.episode === -1
+ ? '?'
+ : (anime.nextAiringEpisode?.episode || 1) - 1}]
+ <span class:countdown={$settings.displayCountdownRightAligned}>
+ {@html airingTime(anime, subsPlease)}
+ </span>
+ {/if}
+ {:else}
<span class:countdown={$settings.displayCountdownRightAligned}>
- {@html airingTime(anime)}
+ {@html airingTime(anime, subsPlease, true)}
</span>
{/if}
- {:else}
- <span class:countdown={$settings.displayCountdownRightAligned}>
- {@html airingTime(anime, true)}
- </span>
- {/if}
+ {/await}
</span>
</li>
{/if}
diff --git a/src/lib/Media/anime.ts b/src/lib/Media/anime.ts
index 927da31b..b9227562 100644
--- a/src/lib/Media/anime.ts
+++ b/src/lib/Media/anime.ts
@@ -5,6 +5,14 @@ import lastPruneTimes from '../../stores/lastPruneTimes';
import type { AniListAuthorisation, UserIdentity } from '../AniList/identity';
import settings from '../../stores/settings';
import type { MediaPrequel } from '$lib/AniList/prequels';
+import type { SubsPlease } from '$lib/subsPlease';
+import levenshtein from 'fast-levenshtein';
+
+interface Time {
+ title: string;
+ time: string;
+ day: string;
+}
export const cleanCache = (user: AniListAuthorisation, identity: UserIdentity) =>
mediaListCollection(user, identity, Type.Anime, get(anime), get(lastPruneTimes).anime, true);
@@ -16,9 +24,85 @@ export const updateMedia = (id: number, progress: number | undefined, callback:
export const totalEpisodes = (anime: Media) =>
anime.episodes === null ? '' : `<span style="opacity: 50%">/${anime.episodes}</span>`;
-export const airingTime = (anime: Media, upcoming = false) => {
+const secondsUntil = (targetTime: string, targetDay: string) => {
+ const now = new Date();
+ const [targetHour, targetMinute] = targetTime.split(':').map(Number);
+ let dayDifference =
+ ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].indexOf(
+ targetDay
+ ) - now.getDay();
+
+ if (dayDifference < 0) dayDifference += 7;
+
+ const targetDate = new Date(now);
+
+ targetDate.setDate(now.getDate() + dayDifference);
+ targetDate.setHours(targetHour, targetMinute, 0, 0);
+
+ const secondsDifference = (Number(targetDate) - Number(now)) / 1000;
+
+ return secondsDifference > 0 ? secondsDifference : secondsDifference + 7 * 24 * 60 * 60;
+};
+
+const normalizeTitle = (title: string) =>
+ (title || '')
+ .toLowerCase()
+ .replace(/season \d+|s\d+/g, '')
+ .trim();
+
+const findClosestMatch = (times: Time[], titles: string[]) => {
+ let closestMatch = null;
+ let smallestDistance = Infinity;
+
+ titles.forEach((animeTitle) => {
+ const normalizedAnimeTitle = normalizeTitle(animeTitle);
+
+ times.forEach((item) => {
+ const normalizedItemTitle = normalizeTitle(item.title);
+ const distance = levenshtein.get(normalizedAnimeTitle, normalizedItemTitle);
+
+ if (distance < smallestDistance) {
+ smallestDistance = distance;
+ closestMatch = item;
+ }
+ });
+ });
+
+ return closestMatch;
+};
+
+export const airingTime = (anime: Media, subsPlease: SubsPlease, upcoming = false) => {
const airingAt = anime.nextAiringEpisode?.airingAt;
- const untilAiring = airingAt ? Math.round((airingAt - Date.now() / 1000) * 100) / 100 : undefined;
+ let untilAiring;
+
+ if (get(settings).displayNativeCountdown) {
+ untilAiring = airingAt ? Math.round((airingAt - Date.now() / 1000) * 100) / 100 : undefined;
+ } else {
+ const times: Time[] = [];
+
+ for (const [key, value] of Object.entries(subsPlease.schedule)) {
+ const flattenedValue = Array.isArray(value) ? value.flat() : [];
+
+ for (const time of flattenedValue) {
+ times.push({
+ title: time.title,
+ time: time.time,
+ day: key
+ });
+ }
+ }
+
+ const time: Time | null = findClosestMatch(times, [
+ anime.title.romaji,
+ anime.title.english,
+ ...anime.synonyms
+ ]);
+
+ if (time) untilAiring = secondsUntil((time as Time).time, (time as Time).day);
+ else
+ untilAiring = airingAt ? Math.round((airingAt - Date.now() / 1000) * 100) / 100 : undefined;
+ }
+
let timeFrame;
const time = new Date(airingAt ? airingAt * 1000 : 0).toLocaleTimeString([], {
hour12: !settings.get().display24HourTime,
diff --git a/src/lib/Tools/SequelSpy.svelte b/src/lib/Tools/SequelSpy.svelte
index 56f40956..e8cc98bf 100644
--- a/src/lib/Tools/SequelSpy.svelte
+++ b/src/lib/Tools/SequelSpy.svelte
@@ -8,6 +8,7 @@
import type { Media } from '$lib/AniList/media';
import { page } from '$app/stores';
import { browser } from '$app/environment';
+ import type { SubsPlease } from '$lib/subsPlease';
export let user: AniListAuthorisation;
@@ -42,7 +43,8 @@
onMount(() => clearAllParameters(['year', 'season']));
- const prequelAiringTime = (prequel: MediaPrequel) => airingTime(prequel as unknown as Media);
+ const prequelAiringTime = (prequel: MediaPrequel, subsPlease: SubsPlease) =>
+ airingTime(prequel as unknown as Media, subsPlease);
</script>
<p>
@@ -66,7 +68,11 @@
</a>
<span style="opacity: 50%;">|</span>
{prequel.seen}<span style="opacity: 50%;">/{prequel.episodes}</span>
- {@html prequelAiringTime(prequel)}
+ {#await fetch(`/api/subsplease?tz=${Intl.DateTimeFormat().resolvedOptions().timeZone}`).then( (r) => r.json() )}
+ Loading ...
+ {:then subsPlease}
+ {@html prequelAiringTime(prequel, subsPlease)}
+ {/await}
</li>
{/each}
</ul>
diff --git a/src/lib/subsPlease.ts b/src/lib/subsPlease.ts
new file mode 100644
index 00000000..b2bcb30e
--- /dev/null
+++ b/src/lib/subsPlease.ts
@@ -0,0 +1,11 @@
+export interface SubsPlease {
+ tz: string;
+ schedule: {
+ [key in string]: {
+ title: string;
+ page: string;
+ image_url: string;
+ time: string;
+ }[];
+ }[];
+}
diff --git a/src/routes/api/subsplease/+server.ts b/src/routes/api/subsplease/+server.ts
index d3e6bf99..91d0e665 100644
--- a/src/routes/api/subsplease/+server.ts
+++ b/src/routes/api/subsplease/+server.ts
@@ -6,5 +6,10 @@ export const GET = async ({ url }) =>
url.searchParams.get('tz') || 'America/Los_Angeles'
}`
)
- ).json()
+ ).json(),
+ {
+ headers: {
+ 'Cache-Control': 'max-age=86400'
+ }
+ }
);
diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte
index e47af157..c4af4cf5 100644
--- a/src/routes/settings/+page.svelte
+++ b/src/routes/settings/+page.svelte
@@ -114,7 +114,11 @@
setting="displayCountdownRightAligned"
text="Display countdown right aligned"
/>
- <SettingCheckboxToggle setting="display24HourTime" text="Display time in 24-hour format" />
+ <SettingCheckboxToggle setting="display24HourTime" text="Display countdown in 24-hour format" />
+ <SettingCheckboxToggle
+ setting="displayNativeCountdown"
+ text="Display countdown as native release"
+ />
<SettingCheckboxToggle
setting="displayNativeTitles"
text="Display titles in native language"
diff --git a/src/stores/settings.ts b/src/stores/settings.ts
index 57d94c6b..b06fe58b 100644
--- a/src/stores/settings.ts
+++ b/src/stores/settings.ts
@@ -27,6 +27,7 @@ export interface Settings {
disableUpcomingAnime: boolean;
display24HourTime: boolean;
displayCountdownRightAligned: boolean;
+ displayNativeCountdown: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
@@ -52,6 +53,7 @@ const defaultSettings: Settings = {
disableUpcomingAnime: false,
display24HourTime: false,
displayCountdownRightAligned: false,
+ displayNativeCountdown: false,
// Calculation
roundDownChapters: true,