From 4b56194ee6807acb56abf0949394efadabf830d4 Mon Sep 17 00:00:00 2001
From: Fuwn <contact@fuwn.net>
Date: Fri, 5 Jun 2026 11:10:22 +0000
Subject: feat(airing): replace SubsPlease with AnimeSchedule (sub+dub)

Source both subbed and dubbed episode schedules from AnimeSchedule.net v3 (absolute timestamps, episode numbers, delay windows, streams), keyed to AniList shows by title. Removes SubsPlease and its ~650-line fuzzy matcher. Countdown source is now a setting (native|sub|dub) with a dub->sub->native fallback.

Requires ANIMESCHEDULE_CLIENT_TOKEN.
---
 src/lib/Schedule/Days.svelte | 91 ++++++++++++++++++++++++++------------------
 1 file changed, 54 insertions(+), 37 deletions(-)

(limited to 'src/lib/Schedule/Days.svelte')

diff --git a/src/lib/Schedule/Days.svelte b/src/lib/Schedule/Days.svelte
index 5a57905a..987bc816 100644
--- a/src/lib/Schedule/Days.svelte
+++ b/src/lib/Schedule/Days.svelte
@@ -3,11 +3,12 @@ import { browser } from "$app/environment";
 import { hydrateMediaListCache } from "$lib/Data/AniList/cacheHydration";
 import type { AniListAuthorisation } from "$lib/Data/AniList/identity";
 import { type Media, mediaListCollection, Type } from "$lib/Data/AniList/media";
-import { findClosestMedia } from "$lib/Media/Anime/Airing/Subtitled/match";
 import type {
-	SubsPlease,
-	SubsPleaseEpisode,
-} from "$lib/Media/Anime/Airing/Subtitled/subsPlease";
+	AiringEntry,
+	AiringSchedule,
+	AirType,
+} from "$lib/Media/Anime/Airing/animeSchedule";
+import { findClosestMedia } from "$lib/Media/Anime/Airing/match";
 import { outboundLink } from "$lib/Media/links";
 import { parseOrDefault } from "$lib/Utility/parameters";
 import settings from "$stores/settings";
@@ -23,7 +24,7 @@ import anime from "$stores/anime";
 import identity from "$stores/identity";
 import lastPruneTimes from "$stores/lastPruneTimes";
 
-export let subsPlease: SubsPlease;
+export let schedule: AiringSchedule;
 export let scheduledMedia: Partial<Media[]>;
 export let forceListMode = false;
 export let user: AniListAuthorisation | undefined;
@@ -35,6 +36,8 @@ let day: string | null = parseOrDefault(urlParameters, "day", null);
 
 let mediaListPromise: Promise<Media[]>;
 
+$: source = ($settings.countdownSource === "dub" ? "dub" : "sub") as AirType;
+
 onMount(async () => {
 	if (user === undefined || $identity.id === -2)
 		mediaListPromise = Promise.resolve([]);
@@ -54,40 +57,54 @@ onMount(async () => {
 	}
 });
 
-const shiftSubsPleaseSchedule = (schedule: SubsPlease["schedule"]) => {
-	const shiftedSchedule: { [key: string]: SubsPleaseEpisode[] } = {};
+const WEEKDAYS = [
+	"Sunday",
+	"Monday",
+	"Tuesday",
+	"Wednesday",
+	"Thursday",
+	"Friday",
+	"Saturday",
+];
+
+const formatTime = (airingAt: number) =>
+	new Date(airingAt * 1000).toLocaleTimeString([], {
+		hour12: !$settings.display24HourTime,
+		hour: "numeric",
+		minute: "2-digit",
+	});
+
+// Bucket releases by the viewer's local weekday, ordered from today onward.
+const scheduleByDay = (entries: AiringEntry[]) => {
+	const buckets: { [key: string]: AiringEntry[] } = {};
+
+	for (const entry of entries) {
+		if (!entry.airingAt) continue;
+
+		const weekday = new Date(entry.airingAt * 1000).toLocaleString("en-us", {
+			weekday: "long",
+		});
 
-	if (day && Object.keys(schedule).includes(day)) {
-		shiftedSchedule[day] = schedule[
-			day as keyof typeof schedule
-		] as unknown as SubsPleaseEpisode[];
+		if (!buckets[weekday]) buckets[weekday] = [];
 
-		return shiftedSchedule;
+		buckets[weekday].push(entry);
 	}
 
-	const days = Object.keys(schedule);
-	const currentDayIndex = days.indexOf(
-		new Date().toLocaleString("en-us", { weekday: "long" }),
-	);
+	for (const weekday in buckets)
+		buckets[weekday].sort((a, b) => a.airingAt - b.airingAt);
 
-	days
-		.slice(currentDayIndex)
-		.concat(days.slice(0, currentDayIndex))
-		.forEach((day) => {
-			const scheduleEntry = schedule[day as keyof typeof schedule];
+	if (day && buckets[day]) return { [day]: buckets[day] };
 
-			shiftedSchedule[day] = Array.isArray(scheduleEntry)
-				? scheduleEntry
-				: ([scheduleEntry] as unknown as SubsPleaseEpisode[]);
-		});
+	const todayIndex = new Date().getDay();
+	const ordered: { [key: string]: AiringEntry[] } = {};
 
-	Object.entries(shiftedSchedule).forEach(([day, scheduleEntry]) => {
-		if (scheduleEntry.length === 0) {
-			delete shiftedSchedule[day];
-		}
-	});
+	for (let offset = 0; offset < 7; offset += 1) {
+		const weekday = WEEKDAYS[(todayIndex + offset) % 7];
+
+		if (buckets[weekday]?.length) ordered[weekday] = buckets[weekday];
+	}
 
-	return shiftedSchedule;
+	return ordered;
 };
 
 const associateMedia = (
@@ -123,7 +140,7 @@ const episode = (media: Media, weekday: string) => {
 
   <Skeleton grid={true} count={7} height="15em" width="49.5%" />
 {:then mediaList}
-  {#each Object.entries(shiftSubsPleaseSchedule(subsPlease.schedule)) as [day, scheduleEntry], dayIndex}
+  {#each Object.entries(scheduleByDay(schedule[source])) as [day, scheduleEntry], dayIndex}
     <details
       open
       class="list"
@@ -136,7 +153,7 @@ const episode = (media: Media, weekday: string) => {
           class="covers"
           style={`grid-template-columns: repeat(auto-fill, minmax(${$settings.displayCoverWidth}px, 1fr))`}
         >
-          {#each Object.values(scheduleEntry) as entry, entryIndex}
+          {#each scheduleEntry as entry, entryIndex}
             {@const media = associateMedia(scheduledMedia, entry.title, mediaList)}
 
             {#if ($settings.displayScheduleFilterList && media) || !$settings.displayScheduleFilterList}
@@ -161,7 +178,7 @@ const episode = (media: Media, weekday: string) => {
                           ? $settings.displayDataSaver
                             ? media.coverImage.medium
                             : media.coverImage.extraLarge
-                          : `https://subsplease.org${entry.image_url}`}
+                          : entry.imageUrl}
                         limit={12.5}
                         alternativeText="Cover"
                       />
@@ -179,7 +196,7 @@ const episode = (media: Media, weekday: string) => {
                           {episode(media, day)}{media.episodes ? `/${media.episodes}` : ''} at
                         </span>
                       {/if}
-                      {entry.time}
+                      {formatTime(entry.airingAt)}
                     </span>
                   </div>
                 </LinkedTooltip>
@@ -189,7 +206,7 @@ const episode = (media: Media, weekday: string) => {
         </div>
       {:else}
         <ol>
-          {#each Object.values(scheduleEntry) as entry}
+          {#each scheduleEntry as entry}
             {@const media = associateMedia(scheduledMedia, entry.title, mediaList)}
 
             {#if ($settings.displayScheduleFilterList && media) || !$settings.displayScheduleFilterList}
@@ -204,7 +221,7 @@ const episode = (media: Media, weekday: string) => {
                       {episode(media, day)}{media.episodes ? `/${media.episodes}` : ''} at
                     </span>
                   {/if}
-                  {entry.time}
+                  {formatTime(entry.airingAt)}
                 </span>
               </li>
             {/if}
-- 
cgit v1.2.3

