diff options
| author | Fuwn <[email protected]> | 2024-10-09 00:41:20 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-10-09 00:41:43 -0700 |
| commit | 998b63a35256ac985a5a2714dd1ca451af4dfd8a (patch) | |
| tree | 50796121a9d5ab0330fdc5d7e098bda2860d9726 /src/lib/Media | |
| parent | feat(graphql): add badgeCount field (diff) | |
| download | due.moe-998b63a35256ac985a5a2714dd1ca451af4dfd8a.tar.xz due.moe-998b63a35256ac985a5a2714dd1ca451af4dfd8a.zip | |
chore(prettier): use spaces instead of tabs
Diffstat (limited to 'src/lib/Media')
| -rw-r--r-- | src/lib/Media/Anime/Airing/AiringTime.svelte | 266 | ||||
| -rw-r--r-- | src/lib/Media/Anime/Airing/Subtitled/match.ts | 344 | ||||
| -rw-r--r-- | src/lib/Media/Anime/Airing/Subtitled/subsPlease.ts | 16 | ||||
| -rw-r--r-- | src/lib/Media/Anime/Airing/time.ts | 222 | ||||
| -rw-r--r-- | src/lib/Media/Anime/cache.ts | 40 | ||||
| -rw-r--r-- | src/lib/Media/Anime/episodes.ts | 2 | ||||
| -rw-r--r-- | src/lib/Media/Anime/season.ts | 18 | ||||
| -rw-r--r-- | src/lib/Media/Cover/HoverCover.svelte | 88 | ||||
| -rw-r--r-- | src/lib/Media/Cover/hoverCover.ts | 76 | ||||
| -rw-r--r-- | src/lib/Media/Manga/cache.ts | 8 | ||||
| -rw-r--r-- | src/lib/Media/Manga/chapters.ts | 386 | ||||
| -rw-r--r-- | src/lib/Media/Manga/volumes.ts | 2 | ||||
| -rw-r--r-- | src/lib/Media/links.ts | 88 |
13 files changed, 778 insertions, 778 deletions
diff --git a/src/lib/Media/Anime/Airing/AiringTime.svelte b/src/lib/Media/Anime/Airing/AiringTime.svelte index 2bbbde84..53a39c39 100644 --- a/src/lib/Media/Anime/Airing/AiringTime.svelte +++ b/src/lib/Media/Anime/Airing/AiringTime.svelte @@ -1,139 +1,139 @@ <script lang="ts"> - import type { Media } from '$lib/Data/AniList/media'; - import settings from '$stores/settings'; - import { onDestroy, onMount } from 'svelte'; - import type { MediaPrequel } from '$lib/Data/AniList/prequels'; - import tooltip from '$lib/Tooltip/tooltip'; - import locale from '$stores/locale'; - - export let originalAnime: Media; - export let upcoming = false; - - const anime = originalAnime; - let opacity = 100; - let timeFrame = ''; - let time = ''; - let nextEpisode = anime.nextAiringEpisode?.episode || 0; - let few = true; - let dateString = ''; - let updateInterval: NodeJS.Timeout; - - onMount(() => { - const setAiringTime = () => { - time = ''; - timeFrame = ''; - dateString = ''; - - const airingAt = anime.nextAiringEpisode?.airingAt; - const untilAiring = airingAt - ? Math.round((airingAt - Date.now() / 1000) * 100) / 100 - : undefined; - let hours = null; - const shortenCountdown = $settings.displayShortCountdown; - - time = new Date(airingAt ? airingAt * 1000 : 0).toLocaleTimeString([], { - hour12: !$settings.display24HourTime, - hour: 'numeric', - minute: '2-digit' - }); - - if ( - (anime as unknown as MediaPrequel).startDate && - new Date( - anime.startDate.year, - (anime as unknown as MediaPrequel).startDate.month, - (anime as unknown as MediaPrequel).startDate.day - ) < new Date() - ) - return `<span class="opaque">on ${new Date( - anime.startDate.year, - (anime as unknown as MediaPrequel).startDate.month, - (anime as unknown as MediaPrequel).startDate.day - ).toLocaleDateString()}</span>`; - - if (untilAiring !== undefined) { - let minutes = Math.round(untilAiring / 60); - - few = true; - - if (minutes > 60) { - hours = minutes / 60; - - if (hours > 24) { - const days = Math.floor(hours / 24); - const weeks = Math.floor(days / 7); - - few = false; - - if (weeks >= 1.5) { - timeFrame = `${weeks}${shortenCountdown ? 'w' : ' week'}${ - weeks === 1 || shortenCountdown ? '' : 's' - }`; - - const residualDays = days % 7; - - if (residualDays > 0) - timeFrame += `${shortenCountdown ? '' : ' '}${residualDays}${ - shortenCountdown ? 'd' : ' day' - }${residualDays === 1 || shortenCountdown ? '' : 's'}`; - } else { - timeFrame += `${days}${shortenCountdown ? 'd' : ' day'}${ - days === 1 || shortenCountdown ? '' : 's' - }`; - } - - const residualHours = Math.floor(hours - days * 24); - - if (residualHours > 0) - timeFrame += `${shortenCountdown ? '' : ' '}${residualHours}${ - shortenCountdown ? 'h' : ' hour' - }${residualHours === 1 || shortenCountdown ? '' : 's'}`; - } else { - const residualMinutes = Math.round(minutes - Math.floor(hours) * 60); - - timeFrame += `${Math.floor(hours).toFixed(0)}${shortenCountdown ? 'h' : ' hour'}${ - Math.floor(hours) === 1 || shortenCountdown ? '' : 's' - }`; - - if (residualMinutes > 0) - timeFrame += `${shortenCountdown ? '' : ' '}${residualMinutes}${ - shortenCountdown ? 'm' : ' minute' - }${residualMinutes === 1 || shortenCountdown ? '' : 's'}`; - } - } else { - minutes = Math.round(minutes); - - timeFrame += `${minutes}${shortenCountdown ? 'm' : ' minute'}${ - minutes === 1 || shortenCountdown ? '' : 's' - }`; - } - - opacity = Math.max(50, 100 - (untilAiring / 60 / 60 / 24 / 7) * 50); - dateString = $locale().dateFormatter(new Date(airingAt ? airingAt * 1000 : 0)); - } - }; - - setAiringTime(); - - updateInterval = setInterval(setAiringTime, 30000); - }); - - onDestroy(() => clearInterval(updateInterval)); + import type { Media } from '$lib/Data/AniList/media'; + import settings from '$stores/settings'; + import { onDestroy, onMount } from 'svelte'; + import type { MediaPrequel } from '$lib/Data/AniList/prequels'; + import tooltip from '$lib/Tooltip/tooltip'; + import locale from '$stores/locale'; + + export let originalAnime: Media; + export let upcoming = false; + + const anime = originalAnime; + let opacity = 100; + let timeFrame = ''; + let time = ''; + let nextEpisode = anime.nextAiringEpisode?.episode || 0; + let few = true; + let dateString = ''; + let updateInterval: NodeJS.Timeout; + + onMount(() => { + const setAiringTime = () => { + time = ''; + timeFrame = ''; + dateString = ''; + + const airingAt = anime.nextAiringEpisode?.airingAt; + const untilAiring = airingAt + ? Math.round((airingAt - Date.now() / 1000) * 100) / 100 + : undefined; + let hours = null; + const shortenCountdown = $settings.displayShortCountdown; + + time = new Date(airingAt ? airingAt * 1000 : 0).toLocaleTimeString([], { + hour12: !$settings.display24HourTime, + hour: 'numeric', + minute: '2-digit' + }); + + if ( + (anime as unknown as MediaPrequel).startDate && + new Date( + anime.startDate.year, + (anime as unknown as MediaPrequel).startDate.month, + (anime as unknown as MediaPrequel).startDate.day + ) < new Date() + ) + return `<span class="opaque">on ${new Date( + anime.startDate.year, + (anime as unknown as MediaPrequel).startDate.month, + (anime as unknown as MediaPrequel).startDate.day + ).toLocaleDateString()}</span>`; + + if (untilAiring !== undefined) { + let minutes = Math.round(untilAiring / 60); + + few = true; + + if (minutes > 60) { + hours = minutes / 60; + + if (hours > 24) { + const days = Math.floor(hours / 24); + const weeks = Math.floor(days / 7); + + few = false; + + if (weeks >= 1.5) { + timeFrame = `${weeks}${shortenCountdown ? 'w' : ' week'}${ + weeks === 1 || shortenCountdown ? '' : 's' + }`; + + const residualDays = days % 7; + + if (residualDays > 0) + timeFrame += `${shortenCountdown ? '' : ' '}${residualDays}${ + shortenCountdown ? 'd' : ' day' + }${residualDays === 1 || shortenCountdown ? '' : 's'}`; + } else { + timeFrame += `${days}${shortenCountdown ? 'd' : ' day'}${ + days === 1 || shortenCountdown ? '' : 's' + }`; + } + + const residualHours = Math.floor(hours - days * 24); + + if (residualHours > 0) + timeFrame += `${shortenCountdown ? '' : ' '}${residualHours}${ + shortenCountdown ? 'h' : ' hour' + }${residualHours === 1 || shortenCountdown ? '' : 's'}`; + } else { + const residualMinutes = Math.round(minutes - Math.floor(hours) * 60); + + timeFrame += `${Math.floor(hours).toFixed(0)}${shortenCountdown ? 'h' : ' hour'}${ + Math.floor(hours) === 1 || shortenCountdown ? '' : 's' + }`; + + if (residualMinutes > 0) + timeFrame += `${shortenCountdown ? '' : ' '}${residualMinutes}${ + shortenCountdown ? 'm' : ' minute' + }${residualMinutes === 1 || shortenCountdown ? '' : 's'}`; + } + } else { + minutes = Math.round(minutes); + + timeFrame += `${minutes}${shortenCountdown ? 'm' : ' minute'}${ + minutes === 1 || shortenCountdown ? '' : 's' + }`; + } + + opacity = Math.max(50, 100 - (untilAiring / 60 / 60 / 24 / 7) * 50); + dateString = $locale().dateFormatter(new Date(airingAt ? airingAt * 1000 : 0)); + } + }; + + setAiringTime(); + + updateInterval = setInterval(setAiringTime, 30000); + }); + + onDestroy(() => clearInterval(updateInterval)); </script> {#if upcoming} - <span title={dateString} use:tooltip style={`opacity: ${opacity}%;`}> - {nextEpisode}{#if anime.episodes !== null}<span class="opaque">/{anime.episodes}</span> - {/if} in {timeFrame} - <span class="opaque"> - {#if few && $settings.displayCoverModeAnime}<br />{/if}{few ? `(${time})` : ''} - </span> - </span> + <span title={dateString} use:tooltip style={`opacity: ${opacity}%;`}> + {nextEpisode}{#if anime.episodes !== null}<span class="opaque">/{anime.episodes}</span> + {/if} in {timeFrame} + <span class="opaque"> + {#if few && $settings.displayCoverModeAnime}<br />{/if}{few ? `(${time})` : ''} + </span> + </span> {:else} - <span title={dateString} use:tooltip style={`opacity: ${opacity}%;`}> - {nextEpisode} in {#if few && $settings.displayCoverModeAnime}<br />{/if}{#if few}<b> - {timeFrame} - </b>{:else}{timeFrame}{/if} - {few ? `(${time})` : ''} - </span> + <span title={dateString} use:tooltip style={`opacity: ${opacity}%;`}> + {nextEpisode} in {#if few && $settings.displayCoverModeAnime}<br />{/if}{#if few}<b> + {timeFrame} + </b>{:else}{timeFrame}{/if} + {few ? `(${time})` : ''} + </span> {/if} diff --git a/src/lib/Media/Anime/Airing/Subtitled/match.ts b/src/lib/Media/Anime/Airing/Subtitled/match.ts index 09daba4f..dd2f45e6 100644 --- a/src/lib/Media/Anime/Airing/Subtitled/match.ts +++ b/src/lib/Media/Anime/Airing/Subtitled/match.ts @@ -7,201 +7,201 @@ import excludeMatch from '$lib/Data/Static/matchExclude.json'; import { season } from '../../season'; export interface Time { - title: string; - time: string; - day: string; + title: string; + time: string; + day: string; } 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(); + 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; + if (dayDifference < 0) dayDifference += 7; - const targetDate = new Date(now); + const targetDate = new Date(now); - targetDate.setDate(now.getDate() + dayDifference); - targetDate.setHours(targetHour, targetMinute, 0, 0); + targetDate.setDate(now.getDate() + dayDifference); + targetDate.setHours(targetHour, targetMinute, 0, 0); - const secondsDifference = (Number(targetDate) - Number(now)) / 1000; + const secondsDifference = (Number(targetDate) - Number(now)) / 1000; - return secondsDifference > 0 ? secondsDifference : secondsDifference + 7 * 24 * 60 * 60; + return secondsDifference > 0 ? secondsDifference : secondsDifference + 7 * 24 * 60 * 60; }; const preprocessTitle = (title: string): string => { - return title - .toLowerCase() - .replace(/\b(season|s|part|cour)\b/g, ' ') - .replace(/[^a-z0-9\s]/gi, '') - .trim() - .split(/\s+/) - .join(' '); + return title + .toLowerCase() + .replace(/\b(season|s|part|cour)\b/g, ' ') + .replace(/[^a-z0-9\s]/gi, '') + .trim() + .split(/\s+/) + .join(' '); }; const calculateWeightedSimilarity = (title1: string, title2: string): number => { - const tokens1 = title1.split(' '); - const tokens2 = title2.split(' '); - const set2 = new Set(tokens2); - let score = 0; - - tokens1.forEach((token) => { - if (set2.has(token)) { - score += /^\d+$/.test(token) ? 2 : 1; - } - }); - - return score / (Math.max(tokens1.length, tokens2.length) * 2); + const tokens1 = title1.split(' '); + const tokens2 = title2.split(' '); + const set2 = new Set(tokens2); + let score = 0; + + tokens1.forEach((token) => { + if (set2.has(token)) { + score += /^\d+$/.test(token) ? 2 : 1; + } + }); + + return score / (Math.max(tokens1.length, tokens2.length) * 2); }; export const findClosestMatch = (times: Time[], anime: Media): Time | null => { - if (excludeMatch.includes(anime.id)) return null; - - let bestMatch: Time | null = null; - let bestScore = 0; - - [anime.title.romaji, anime.title.english, ...anime.synonyms] - .filter(Boolean) - .forEach((searchTitle) => { - if (searchTitle.includes('OVA') || searchTitle.includes('Special')) return; - - const normalizedSearchTitle = preprocessTitle(searchTitle); - - times.forEach((time) => { - const normalizedTimeTitle = preprocessTitle(time.title); - const similarityScore = calculateWeightedSimilarity( - normalizedSearchTitle, - normalizedTimeTitle - ); - - if ( - similarityScore > bestScore && - time.day === - new Date((anime.nextAiringEpisode?.airingAt || 0) * 1000).toLocaleString('en-US', { - weekday: 'long' - }) - ) { - bestScore = similarityScore; - bestMatch = time; - } - }); - }); - - return bestMatch; + if (excludeMatch.includes(anime.id)) return null; + + let bestMatch: Time | null = null; + let bestScore = 0; + + [anime.title.romaji, anime.title.english, ...anime.synonyms] + .filter(Boolean) + .forEach((searchTitle) => { + if (searchTitle.includes('OVA') || searchTitle.includes('Special')) return; + + const normalizedSearchTitle = preprocessTitle(searchTitle); + + times.forEach((time) => { + const normalizedTimeTitle = preprocessTitle(time.title); + const similarityScore = calculateWeightedSimilarity( + normalizedSearchTitle, + normalizedTimeTitle + ); + + if ( + similarityScore > bestScore && + time.day === + new Date((anime.nextAiringEpisode?.airingAt || 0) * 1000).toLocaleString('en-US', { + weekday: 'long' + }) + ) { + bestScore = similarityScore; + bestMatch = time; + } + }); + }); + + return bestMatch; }; const normalizeTitle = (title: string | null) => - (title || '') - .toLowerCase() - .replace(/\b(s|season|part|cour)\s*\d+/g, '') - .replace(/[\W_]+/g, ' ') - .trim(); + (title || '') + .toLowerCase() + .replace(/\b(s|season|part|cour)\s*\d+/g, '') + .replace(/[\W_]+/g, ' ') + .trim(); export const findClosestMedia = (media: Media[], matchFor: string) => { - if (!matchFor) return null; - - let bestFitMedia: Media | null = null; - let smallestDistance = -Infinity; - - media.forEach((m) => { - const titles = [m.title.romaji, m.title.english, ...m.synonyms].filter(Boolean); - - if ( - titles.some( - (title) => title.toLowerCase().includes('special') || title.toLowerCase().includes('ova') - ) - ) - return; - - titles.forEach((title) => { - const normalisedTitle = normalizeTitle(title); - const normalisedMatchFor = normalizeTitle(matchFor); - const distance = stringSimilarity.compareTwoStrings(normalisedMatchFor, normalisedTitle); - - if ( - distance > smallestDistance && - (normalisedMatchFor - .split(' ') - .filter((word) => titles.some((title) => normalizeTitle(title).includes(word))).length >= - normalisedMatchFor.split(' ').length || - titles.some((title) => normalizeTitle(title).includes(normalisedMatchFor))) - ) { - smallestDistance = distance; - bestFitMedia = m; - } - }); - }); - - return bestFitMedia as Media | null; + if (!matchFor) return null; + + let bestFitMedia: Media | null = null; + let smallestDistance = -Infinity; + + media.forEach((m) => { + const titles = [m.title.romaji, m.title.english, ...m.synonyms].filter(Boolean); + + if ( + titles.some( + (title) => title.toLowerCase().includes('special') || title.toLowerCase().includes('ova') + ) + ) + return; + + titles.forEach((title) => { + const normalisedTitle = normalizeTitle(title); + const normalisedMatchFor = normalizeTitle(matchFor); + const distance = stringSimilarity.compareTwoStrings(normalisedMatchFor, normalisedTitle); + + if ( + distance > smallestDistance && + (normalisedMatchFor + .split(' ') + .filter((word) => titles.some((title) => normalizeTitle(title).includes(word))).length >= + normalisedMatchFor.split(' ').length || + titles.some((title) => normalizeTitle(title).includes(normalisedMatchFor))) + ) { + smallestDistance = distance; + bestFitMedia = m; + } + }); + }); + + return bestFitMedia as Media | null; }; export const injectAiringTime = (anime: Media, subsPlease: SubsPlease | null) => { - if (season() !== anime.season) return anime; - - const airingAt = anime.nextAiringEpisode?.airingAt; - const now = new Date(); - // const nativeUntilAiring = airingAt - // ? Math.round((airingAt - Date.now() / 1000) * 100) / 100 - // : undefined; - const nativeTime = new Date(airingAt ? airingAt * 1000 : 0); - let untilAiring; - let time = new Date(airingAt ? airingAt * 1000 : 0); - let nextEpisode = anime.nextAiringEpisode?.episode || 0; - - if ( - !( - (get(settings).displayNativeCountdown || !subsPlease) - // || !(nativeUntilAiring !== undefined && nativeUntilAiring < 24 * 60 * 60) - ) - ) { - 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 - }); - } - } - - if ((anime.nextAiringEpisode?.episode || 0) > 1) { - const foundTime: Time | null = findClosestMatch(times, anime); - - if (foundTime) { - untilAiring = secondsUntil((foundTime as Time).time, (foundTime as Time).day); - time = new Date(Date.now() + untilAiring * 1000); - } - } - } - - const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000; - - if (nativeTime > time) { - nextEpisode -= 1; - } - - if (nativeTime.getTime() - now.getTime() > SEVEN_DAYS) { - const beforeTime = time; - - time = nativeTime; - - time.setHours(beforeTime.getHours()); - time.setMinutes(beforeTime.getMinutes()); - } - - return { - ...anime, - nextAiringEpisode: { - episode: nextEpisode, - airingAt: time.getTime() / 1000, - nativeAiringAt: nativeTime.getTime() / 1000 - } - } as Media; + if (season() !== anime.season) return anime; + + const airingAt = anime.nextAiringEpisode?.airingAt; + const now = new Date(); + // const nativeUntilAiring = airingAt + // ? Math.round((airingAt - Date.now() / 1000) * 100) / 100 + // : undefined; + const nativeTime = new Date(airingAt ? airingAt * 1000 : 0); + let untilAiring; + let time = new Date(airingAt ? airingAt * 1000 : 0); + let nextEpisode = anime.nextAiringEpisode?.episode || 0; + + if ( + !( + (get(settings).displayNativeCountdown || !subsPlease) + // || !(nativeUntilAiring !== undefined && nativeUntilAiring < 24 * 60 * 60) + ) + ) { + 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 + }); + } + } + + if ((anime.nextAiringEpisode?.episode || 0) > 1) { + const foundTime: Time | null = findClosestMatch(times, anime); + + if (foundTime) { + untilAiring = secondsUntil((foundTime as Time).time, (foundTime as Time).day); + time = new Date(Date.now() + untilAiring * 1000); + } + } + } + + const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000; + + if (nativeTime > time) { + nextEpisode -= 1; + } + + if (nativeTime.getTime() - now.getTime() > SEVEN_DAYS) { + const beforeTime = time; + + time = nativeTime; + + time.setHours(beforeTime.getHours()); + time.setMinutes(beforeTime.getMinutes()); + } + + return { + ...anime, + nextAiringEpisode: { + episode: nextEpisode, + airingAt: time.getTime() / 1000, + nativeAiringAt: nativeTime.getTime() / 1000 + } + } as Media; }; diff --git a/src/lib/Media/Anime/Airing/Subtitled/subsPlease.ts b/src/lib/Media/Anime/Airing/Subtitled/subsPlease.ts index 3815259d..69f56286 100644 --- a/src/lib/Media/Anime/Airing/Subtitled/subsPlease.ts +++ b/src/lib/Media/Anime/Airing/Subtitled/subsPlease.ts @@ -1,13 +1,13 @@ export interface SubsPlease { - tz: string; - schedule: { - [key in string]: SubsPleaseEpisode; - }[]; + tz: string; + schedule: { + [key in string]: SubsPleaseEpisode; + }[]; } export interface SubsPleaseEpisode { - title: string; - page: string; - image_url: string; - time: string; + title: string; + page: string; + image_url: string; + time: string; } diff --git a/src/lib/Media/Anime/Airing/time.ts b/src/lib/Media/Anime/Airing/time.ts index dcaac944..76d51668 100644 --- a/src/lib/Media/Anime/Airing/time.ts +++ b/src/lib/Media/Anime/Airing/time.ts @@ -7,116 +7,116 @@ import { totalEpisodes } from '../episodes'; import { get } from 'svelte/store'; export const airingTime = ( - originalAnime: Media, - subsPlease: SubsPlease | null, - upcoming = false, - forceDays = false + originalAnime: Media, + subsPlease: SubsPlease | null, + upcoming = false, + forceDays = false ) => { - const anime = injectAiringTime(originalAnime, subsPlease); - const airingAt = anime.nextAiringEpisode?.airingAt; - const untilAiring = airingAt ? Math.round((airingAt - Date.now() / 1000) * 100) / 100 : undefined; - const time = new Date(airingAt ? airingAt * 1000 : 0).toLocaleTimeString([], { - hour12: !settings.get().display24HourTime, - hour: 'numeric', - minute: '2-digit' - }); - let timeFrame = ''; - let hours = null; - const shortenCountdown = get(settings).displayShortCountdown; - - if ( - (anime as unknown as MediaPrequel).startDate && - new Date( - anime.startDate.year, - (anime as unknown as MediaPrequel).startDate.month, - (anime as unknown as MediaPrequel).startDate.day - ) < new Date() - ) - return `<span class="opaque">on ${new Date( - anime.startDate.year, - (anime as unknown as MediaPrequel).startDate.month, - (anime as unknown as MediaPrequel).startDate.day - ).toLocaleDateString()}</span>`; - - if (untilAiring !== undefined) { - let minutes = untilAiring / 60; - let few = true; - - if (minutes > 60) { - hours = minutes / 60; - - if (hours > 24) { - let weeks = Math.floor(hours / 24) / 7; - - few = false; - - if (weeks >= 1.5 && !forceDays) { - weeks = Math.round(weeks); - - timeFrame = `${weeks}${shortenCountdown ? 'w' : ' week'}${ - weeks === 1 || shortenCountdown ? '' : 's' - }`; - } else { - const days = Math.round(Math.floor(hours / 24)); - const residualHours = Math.floor(hours - days * 24); - - timeFrame += `${days.toFixed(0)}${shortenCountdown ? 'd' : ' day'}${ - days === 1 || shortenCountdown ? '' : 's' - }`; - - if (residualHours > 0) - timeFrame += `${shortenCountdown ? '' : ' '}${residualHours}${ - shortenCountdown ? 'h' : ' hour' - }${residualHours === 1 || shortenCountdown ? '' : 's'}`; - } - } else { - const residualMinutes = Math.round(minutes - Math.floor(hours) * 60); - - timeFrame += `${hours.toFixed(0)}${shortenCountdown ? 'h' : ' hour'}${ - hours === 1 || shortenCountdown ? '' : 's' - }`; - - if (residualMinutes > 0) - timeFrame += `${shortenCountdown ? '' : ' '}${residualMinutes}${ - shortenCountdown ? 'm' : ' minute' - }${residualMinutes === 1 || shortenCountdown ? '' : 's'}`; - } - } else { - minutes = Math.round(minutes); - - timeFrame += `${minutes}${shortenCountdown ? 'm' : ' minute'}${ - minutes === 1 || shortenCountdown ? '' : 's' - }`; - } - - const opacity = Math.max(50, 100 - (untilAiring / 60 / 60 / 24 / 7) * 50); - const nextEpisode = - anime.nextAiringEpisode?.nativeAiringAt && - !upcoming && - anime.nextAiringEpisode.nativeAiringAt < Date.now() / 1000 + 1 * 24 * 60 * 60 - ? anime.nextAiringEpisode.episode - 1 - : anime.nextAiringEpisode?.episode || 0; - const dateString = - new Date(airingAt ? airingAt * 1000 : 0).toLocaleDateString([], { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' - }) + - ' ' + - time; - - if (upcoming) - return `<span title="${dateString}" style="opacity: ${opacity}%;">${nextEpisode}${totalEpisodes( - anime - )} in ${timeFrame} <span class="opaque">${ - few && get(settings).displayCoverModeAnime ? '<br>' : '' - }${few ? `(${time})` : ''}</span></span>`; - else - return `<span title="${dateString}" style="opacity: ${opacity}%;">${nextEpisode} in ${ - few && get(settings).displayCoverModeAnime ? '<br>' : '' - }${few ? '<b>' : ''}${timeFrame}${few ? '</b>' : ''} ${few ? `(${time})` : ''}</span>`; - } - - return ''; + const anime = injectAiringTime(originalAnime, subsPlease); + const airingAt = anime.nextAiringEpisode?.airingAt; + const untilAiring = airingAt ? Math.round((airingAt - Date.now() / 1000) * 100) / 100 : undefined; + const time = new Date(airingAt ? airingAt * 1000 : 0).toLocaleTimeString([], { + hour12: !settings.get().display24HourTime, + hour: 'numeric', + minute: '2-digit' + }); + let timeFrame = ''; + let hours = null; + const shortenCountdown = get(settings).displayShortCountdown; + + if ( + (anime as unknown as MediaPrequel).startDate && + new Date( + anime.startDate.year, + (anime as unknown as MediaPrequel).startDate.month, + (anime as unknown as MediaPrequel).startDate.day + ) < new Date() + ) + return `<span class="opaque">on ${new Date( + anime.startDate.year, + (anime as unknown as MediaPrequel).startDate.month, + (anime as unknown as MediaPrequel).startDate.day + ).toLocaleDateString()}</span>`; + + if (untilAiring !== undefined) { + let minutes = untilAiring / 60; + let few = true; + + if (minutes > 60) { + hours = minutes / 60; + + if (hours > 24) { + let weeks = Math.floor(hours / 24) / 7; + + few = false; + + if (weeks >= 1.5 && !forceDays) { + weeks = Math.round(weeks); + + timeFrame = `${weeks}${shortenCountdown ? 'w' : ' week'}${ + weeks === 1 || shortenCountdown ? '' : 's' + }`; + } else { + const days = Math.round(Math.floor(hours / 24)); + const residualHours = Math.floor(hours - days * 24); + + timeFrame += `${days.toFixed(0)}${shortenCountdown ? 'd' : ' day'}${ + days === 1 || shortenCountdown ? '' : 's' + }`; + + if (residualHours > 0) + timeFrame += `${shortenCountdown ? '' : ' '}${residualHours}${ + shortenCountdown ? 'h' : ' hour' + }${residualHours === 1 || shortenCountdown ? '' : 's'}`; + } + } else { + const residualMinutes = Math.round(minutes - Math.floor(hours) * 60); + + timeFrame += `${hours.toFixed(0)}${shortenCountdown ? 'h' : ' hour'}${ + hours === 1 || shortenCountdown ? '' : 's' + }`; + + if (residualMinutes > 0) + timeFrame += `${shortenCountdown ? '' : ' '}${residualMinutes}${ + shortenCountdown ? 'm' : ' minute' + }${residualMinutes === 1 || shortenCountdown ? '' : 's'}`; + } + } else { + minutes = Math.round(minutes); + + timeFrame += `${minutes}${shortenCountdown ? 'm' : ' minute'}${ + minutes === 1 || shortenCountdown ? '' : 's' + }`; + } + + const opacity = Math.max(50, 100 - (untilAiring / 60 / 60 / 24 / 7) * 50); + const nextEpisode = + anime.nextAiringEpisode?.nativeAiringAt && + !upcoming && + anime.nextAiringEpisode.nativeAiringAt < Date.now() / 1000 + 1 * 24 * 60 * 60 + ? anime.nextAiringEpisode.episode - 1 + : anime.nextAiringEpisode?.episode || 0; + const dateString = + new Date(airingAt ? airingAt * 1000 : 0).toLocaleDateString([], { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }) + + ' ' + + time; + + if (upcoming) + return `<span title="${dateString}" style="opacity: ${opacity}%;">${nextEpisode}${totalEpisodes( + anime + )} in ${timeFrame} <span class="opaque">${ + few && get(settings).displayCoverModeAnime ? '<br>' : '' + }${few ? `(${time})` : ''}</span></span>`; + else + return `<span title="${dateString}" style="opacity: ${opacity}%;">${nextEpisode} in ${ + few && get(settings).displayCoverModeAnime ? '<br>' : '' + }${few ? '<b>' : ''}${timeFrame}${few ? '</b>' : ''} ${few ? `(${time})` : ''}</span>`; + } + + return ''; }; diff --git a/src/lib/Media/Anime/cache.ts b/src/lib/Media/Anime/cache.ts index a47a655e..9aabb2ab 100644 --- a/src/lib/Media/Anime/cache.ts +++ b/src/lib/Media/Anime/cache.ts @@ -5,27 +5,27 @@ import lastPruneTimes from '$stores/lastPruneTimes'; import type { AniListAuthorisation, UserIdentity } from '../../Data/AniList/identity'; export const cleanCache = (user: AniListAuthorisation, identity: UserIdentity) => - mediaListCollection(user, identity, Type.Anime, get(anime), get(lastPruneTimes).anime, { - forcePrune: true - }); + mediaListCollection(user, identity, Type.Anime, get(anime), get(lastPruneTimes).anime, { + forcePrune: true + }); export const incrementMediaProgress = ( - id: number, - progress: number | undefined, - user: AniListAuthorisation, - callback: () => void + id: number, + progress: number | undefined, + user: AniListAuthorisation, + callback: () => void ) => { - fetch('https://graphql.anilist.co', { - method: 'POST', - headers: { - Authorization: `${user.tokenType} ${user.accessToken}`, - 'Content-Type': 'application/json', - Accept: 'application/json' - }, - body: JSON.stringify({ - query: `mutation { SaveMediaListEntry(mediaId: ${id}, progress: ${ - (progress || 0) + 1 - }) { id } }` - }) - }).then(callback); + fetch('https://graphql.anilist.co', { + method: 'POST', + headers: { + Authorization: `${user.tokenType} ${user.accessToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + body: JSON.stringify({ + query: `mutation { SaveMediaListEntry(mediaId: ${id}, progress: ${ + (progress || 0) + 1 + }) { id } }` + }) + }).then(callback); }; diff --git a/src/lib/Media/Anime/episodes.ts b/src/lib/Media/Anime/episodes.ts index 3cdbdb98..f4994f83 100644 --- a/src/lib/Media/Anime/episodes.ts +++ b/src/lib/Media/Anime/episodes.ts @@ -1,4 +1,4 @@ import type { Media } from '$lib/Data/AniList/media'; export const totalEpisodes = (anime: Media) => - anime.episodes === null ? '' : `<span class="opaque">/${anime.episodes}</span>`; + anime.episodes === null ? '' : `<span class="opaque">/${anime.episodes}</span>`; diff --git a/src/lib/Media/Anime/season.ts b/src/lib/Media/Anime/season.ts index d7922e2b..d0cd6c25 100644 --- a/src/lib/Media/Anime/season.ts +++ b/src/lib/Media/Anime/season.ts @@ -1,11 +1,11 @@ export const season = () => { - if (new Date().getMonth() >= 0 && new Date().getMonth() <= 2) - return 'WINTER' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'; - else if (new Date().getMonth() >= 3 && new Date().getMonth() <= 5) - return 'SPRING' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'; - else if (new Date().getMonth() >= 6 && new Date().getMonth() <= 8) - return 'SUMMER' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'; - else if (new Date().getMonth() >= 9 && new Date().getMonth() <= 11) - return 'FALL' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'; - else return 'WINTER' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'; + if (new Date().getMonth() >= 0 && new Date().getMonth() <= 2) + return 'WINTER' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'; + else if (new Date().getMonth() >= 3 && new Date().getMonth() <= 5) + return 'SPRING' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'; + else if (new Date().getMonth() >= 6 && new Date().getMonth() <= 8) + return 'SUMMER' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'; + else if (new Date().getMonth() >= 9 && new Date().getMonth() <= 11) + return 'FALL' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'; + else return 'WINTER' as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL'; }; diff --git a/src/lib/Media/Cover/HoverCover.svelte b/src/lib/Media/Cover/HoverCover.svelte index eecbabe5..51cbf5d2 100644 --- a/src/lib/Media/Cover/HoverCover.svelte +++ b/src/lib/Media/Cover/HoverCover.svelte @@ -1,53 +1,53 @@ <script lang="ts"> - import settings from '$stores/settings'; - import type { HoverCoverResponse } from './hoverCover'; + import settings from '$stores/settings'; + import type { HoverCoverResponse } from './hoverCover'; - export let options: HoverCoverResponse; - export let width = 250; + export let options: HoverCoverResponse; + export let width = 250; </script> {#if options.hovering} - <img - class="hover-image show card card-small" - src={options.media - ? $settings.displayDataSaver - ? options.media.coverImage.medium - : options.media.coverImage.extraLarge - : `https://subsplease.org${options.item?.image_url}`} - alt="Media Cover" - loading="lazy" - style={`width: ${width}px; ${options.style}`} - /> + <img + class="hover-image show card card-small" + src={options.media + ? $settings.displayDataSaver + ? options.media.coverImage.medium + : options.media.coverImage.extraLarge + : `https://subsplease.org${options.item?.image_url}`} + alt="Media Cover" + loading="lazy" + style={`width: ${width}px; ${options.style}`} + /> {/if} <style lang="scss"> - $coverTransitionTime: 200ms; - - .hover-image { - position: fixed; - height: auto; - display: none; - border-radius: 8px; - transition: opacity $coverTransitionTime ease-in-out, top 0.3s ease, left 0.3s ease; - } - - .show { - display: block; - } - - .hover-image { - animation: dropIn $coverTransitionTime ease-in-out; - } - - @keyframes dropIn { - 0% { - opacity: 0; - transform: translateY(-1rem); - } - - 100% { - opacity: 1; - transform: translateY(0); - } - } + $coverTransitionTime: 200ms; + + .hover-image { + position: fixed; + height: auto; + display: none; + border-radius: 8px; + transition: opacity $coverTransitionTime ease-in-out, top 0.3s ease, left 0.3s ease; + } + + .show { + display: block; + } + + .hover-image { + animation: dropIn $coverTransitionTime ease-in-out; + } + + @keyframes dropIn { + 0% { + opacity: 0; + transform: translateY(-1rem); + } + + 100% { + opacity: 1; + transform: translateY(0); + } + } </style> diff --git a/src/lib/Media/Cover/hoverCover.ts b/src/lib/Media/Cover/hoverCover.ts index c2b6697c..3b364349 100644 --- a/src/lib/Media/Cover/hoverCover.ts +++ b/src/lib/Media/Cover/hoverCover.ts @@ -4,53 +4,53 @@ import type { SubsPleaseEpisode } from '../Anime/Airing/Subtitled/subsPlease'; import settings from '$stores/settings'; export interface HoverCoverResponse { - // OnMouseEnterLeave - hovering?: boolean; - item?: SubsPleaseEpisode | null; - media?: Media | null; - - // OnMouseMove - height?: number; - style?: string; + // OnMouseEnterLeave + hovering?: boolean; + item?: SubsPleaseEpisode | null; + media?: Media | null; + + // OnMouseMove + height?: number; + style?: string; } export const onMouseEnter = ( - media: Media | Partial<Media> | null, - item: SubsPleaseEpisode | null = null + media: Media | Partial<Media> | null, + item: SubsPleaseEpisode | null = null ) => { - if (!get(settings).displayHoverCover && !item) - return { hovering: false, item: null, media: null } as HoverCoverResponse; + if (!get(settings).displayHoverCover && !item) + return { hovering: false, item: null, media: null } as HoverCoverResponse; - return { hovering: true, item, media } as HoverCoverResponse; + return { hovering: true, item, media } as HoverCoverResponse; }; export const onMouseLeave = () => { - return { hovering: false, item: null, media: null } as HoverCoverResponse; + return { hovering: false, item: null, media: null } as HoverCoverResponse; }; export const onMouseMove = (event: MouseEvent, imageWidth = 250) => { - const offset = 10; - let imageLeft = 0; - let imageTop = 0; - const elements = document.getElementsByClassName('hover-image'); - - if (elements.length === 0) return { height: 0, style: '' } as HoverCoverResponse; - - const response: HoverCoverResponse = { - height: (elements[0] as HTMLImageElement).height, - style: '' - }; - const height = response.height || 0; - - imageLeft = - event.pageX + height + offset > window.innerWidth - ? event.pageX - imageWidth - offset - : event.pageX + offset; - imageTop = - event.pageY - window.scrollY + height + offset > window.innerHeight - ? event.pageY - window.scrollY - height - offset - : event.pageY - window.scrollY + offset; - response.style = `top: ${imageTop}px; left: ${imageLeft}px;`; - - return response; + const offset = 10; + let imageLeft = 0; + let imageTop = 0; + const elements = document.getElementsByClassName('hover-image'); + + if (elements.length === 0) return { height: 0, style: '' } as HoverCoverResponse; + + const response: HoverCoverResponse = { + height: (elements[0] as HTMLImageElement).height, + style: '' + }; + const height = response.height || 0; + + imageLeft = + event.pageX + height + offset > window.innerWidth + ? event.pageX - imageWidth - offset + : event.pageX + offset; + imageTop = + event.pageY - window.scrollY + height + offset > window.innerHeight + ? event.pageY - window.scrollY - height - offset + : event.pageY - window.scrollY + offset; + response.style = `top: ${imageTop}px; left: ${imageLeft}px;`; + + return response; }; diff --git a/src/lib/Media/Manga/cache.ts b/src/lib/Media/Manga/cache.ts index d35bf64a..6bd248dc 100644 --- a/src/lib/Media/Manga/cache.ts +++ b/src/lib/Media/Manga/cache.ts @@ -2,9 +2,9 @@ import { database } from '../../Database/IDB/chapters'; import manga from '$stores/manga'; export const pruneAllManga = async () => { - const all = await database.chapters.toArray(); - const ids = all.map((m) => m.id); + const all = await database.chapters.toArray(); + const ids = all.map((m) => m.id); - manga.set(''); - await database.chapters.bulkDelete(ids); + manga.set(''); + await database.chapters.bulkDelete(ids); }; diff --git a/src/lib/Media/Manga/chapters.ts b/src/lib/Media/Manga/chapters.ts index a601fdc8..32e1f0fc 100644 --- a/src/lib/Media/Manga/chapters.ts +++ b/src/lib/Media/Manga/chapters.ts @@ -6,202 +6,202 @@ import type { UserIdentity } from '../../Data/AniList/identity'; import { database } from '../../Database/IDB/chapters'; const getManga = async ( - statusIn: string, - year: number, - native: string | null, - english: string | null, - romaji: string | null + statusIn: string, + year: number, + native: string | null, + english: string | null, + romaji: string | null ) => { - let status = ''; - let error = false; - - switch (statusIn) { - case 'FINISHED': - { - status = 'completed'; - } - break; - case 'RELEASING': - { - status = 'ongoing'; - } - break; - case 'HIATUS': - { - status = 'hiatus'; - } - break; - case 'CANCELLED': - { - status = 'cancelled'; - } - break; - } - - const nullIfNullString = (s: string | null) => (s == 'null' ? null : s); - const get = async (title: string) => { - try { - return await ( - await fetch( - proxy( - `https://api.mangadex.org/manga?title=${encodeURIComponent( - title - )}&year=${year}&status[]=${status}` - ) - ) - ).json(); - } catch { - error = true; - } - }; - - let mangadexData = await get( - nullIfNullString(native) || nullIfNullString(english) || nullIfNullString(romaji) || '' - ); - - if (error) return new Response('rate-limited'); - - if (mangadexData['data'] === undefined || mangadexData['data'].length === 0) { - mangadexData = await get(nullIfNullString(english) || ''); - - if (mangadexData['data'] === undefined || mangadexData['data'].length === 0) { - mangadexData = await get(nullIfNullString(romaji) || ''); - } - } - - return Response.json(mangadexData, { - headers: { - 'Cache-Control': 'max-age=300' - } - }); + let status = ''; + let error = false; + + switch (statusIn) { + case 'FINISHED': + { + status = 'completed'; + } + break; + case 'RELEASING': + { + status = 'ongoing'; + } + break; + case 'HIATUS': + { + status = 'hiatus'; + } + break; + case 'CANCELLED': + { + status = 'cancelled'; + } + break; + } + + const nullIfNullString = (s: string | null) => (s == 'null' ? null : s); + const get = async (title: string) => { + try { + return await ( + await fetch( + proxy( + `https://api.mangadex.org/manga?title=${encodeURIComponent( + title + )}&year=${year}&status[]=${status}` + ) + ) + ).json(); + } catch { + error = true; + } + }; + + let mangadexData = await get( + nullIfNullString(native) || nullIfNullString(english) || nullIfNullString(romaji) || '' + ); + + if (error) return new Response('rate-limited'); + + if (mangadexData['data'] === undefined || mangadexData['data'].length === 0) { + mangadexData = await get(nullIfNullString(english) || ''); + + if (mangadexData['data'] === undefined || mangadexData['data'].length === 0) { + mangadexData = await get(nullIfNullString(romaji) || ''); + } + } + + return Response.json(mangadexData, { + headers: { + 'Cache-Control': 'max-age=300' + } + }); }; export const chapterCount = async ( - identity: UserIdentity, - manga: Media, - disableGuessing: boolean - // preferActivity = false + identity: UserIdentity, + manga: Media, + disableGuessing: boolean + // preferActivity = false ): Promise<number | null> => { - const chapters = await database.chapters.get(manga.id); - - if (chapters !== undefined) return chapters.chapters === -1 ? null : chapters.chapters; - - // if (preferActivity) { - // return await recentMediaActivities(identity, manga); - // } - - const tryRecentMediaActivities = async () => { - if (disableGuessing) { - await database.chapters.put({ - id: manga.id, - chapters: -1, - volumes: null - }); - - return null; - } - - const anilistData = await recentMediaActivities( - identity, - manga, - settings.get().calculateGuessMethod - ); - - await database.chapters.put({ - id: manga.id, - chapters: anilistData ? anilistData : -1, - volumes: null - }); - - return anilistData; - }; - - if (manga.format === 'NOVEL') return await tryRecentMediaActivities(); - - let lastChapter = 0; - let completedVolumes = null; - - if (!settings.get().calculatePreferNativeChapterCount) { - const mangadexData = await getManga( - manga.status, - manga.startDate.year, - manga.title.native, - manga.title.english, - manga.title.romaji - ); - - if ((await mangadexData.clone().text()) === 'rate-limited') return -22; - - const mangadexDataJson = await mangadexData.json(); - - if (mangadexDataJson['data'] === undefined || mangadexDataJson['data'].length === 0) - return await tryRecentMediaActivities(); - - const mangadexId = mangadexDataJson['data'][0]['id']; - const lastChapterDataJson = await ( - await fetch( - proxy( - `https://api.mangadex.org/manga/${mangadexId}/feed?order[chapter]=desc&translatedLanguage[]=en&limit=1&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic` - ) - ) - ).json(); - - if (lastChapterDataJson['data'] === undefined || lastChapterDataJson['data'].length === 0) - return await tryRecentMediaActivities(); - - lastChapter = lastChapterDataJson['data'][0]['attributes']['chapter']; - completedVolumes = null; - - if ((manga.mediaListEntry || { progress: 0 }).progress > lastChapter && !disableGuessing) { - const anilistData = await recentMediaActivities( - identity, - manga, - settings.get().calculateGuessMethod - ); - - if (anilistData !== null && anilistData > lastChapter) lastChapter = anilistData; - } - - if (!settings.get().calculateDisableOutOfDateVolumeWarning) { - const volumeOfChapterData = await ( - await fetch( - proxy( - `https://api.mangadex.org/chapter?manga=${mangadexId}&chapter=${manga.mediaListEntry?.progress}&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic&limit=1` - ) - ) - ).json(); - let lastAvailableVolume = lastChapterDataJson['data'][0]['attributes']['volume']; - - if (lastAvailableVolume === null) { - let chapterIndex = 0; - - while (chapterIndex < lastChapterDataJson['data'].length && lastAvailableVolume === null) { - if (lastChapterDataJson['data'][chapterIndex]['attributes']['volume'] !== null) - lastAvailableVolume = lastChapterDataJson['data'][chapterIndex]['attributes']['volume']; - - chapterIndex += 1; - } - } - - if (volumeOfChapterData['data'] !== undefined && volumeOfChapterData['data'].length > 0) { - const volumeOfChapter = volumeOfChapterData['data'][0]['attributes']['volume']; - - if (volumeOfChapter !== null) completedVolumes = volumeOfChapter; - - if (completedVolumes === volumeOfChapter) completedVolumes -= 1; - } - } - } else { - lastChapter = (await getChapterCount(manga.title.native)) || 0; - } - - if (lastChapter == 0) lastChapter = -1; - - await database.chapters.put({ - id: manga.id, - chapters: Number(lastChapter), - volumes: completedVolumes - }); - - return Number(lastChapter); + const chapters = await database.chapters.get(manga.id); + + if (chapters !== undefined) return chapters.chapters === -1 ? null : chapters.chapters; + + // if (preferActivity) { + // return await recentMediaActivities(identity, manga); + // } + + const tryRecentMediaActivities = async () => { + if (disableGuessing) { + await database.chapters.put({ + id: manga.id, + chapters: -1, + volumes: null + }); + + return null; + } + + const anilistData = await recentMediaActivities( + identity, + manga, + settings.get().calculateGuessMethod + ); + + await database.chapters.put({ + id: manga.id, + chapters: anilistData ? anilistData : -1, + volumes: null + }); + + return anilistData; + }; + + if (manga.format === 'NOVEL') return await tryRecentMediaActivities(); + + let lastChapter = 0; + let completedVolumes = null; + + if (!settings.get().calculatePreferNativeChapterCount) { + const mangadexData = await getManga( + manga.status, + manga.startDate.year, + manga.title.native, + manga.title.english, + manga.title.romaji + ); + + if ((await mangadexData.clone().text()) === 'rate-limited') return -22; + + const mangadexDataJson = await mangadexData.json(); + + if (mangadexDataJson['data'] === undefined || mangadexDataJson['data'].length === 0) + return await tryRecentMediaActivities(); + + const mangadexId = mangadexDataJson['data'][0]['id']; + const lastChapterDataJson = await ( + await fetch( + proxy( + `https://api.mangadex.org/manga/${mangadexId}/feed?order[chapter]=desc&translatedLanguage[]=en&limit=1&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic` + ) + ) + ).json(); + + if (lastChapterDataJson['data'] === undefined || lastChapterDataJson['data'].length === 0) + return await tryRecentMediaActivities(); + + lastChapter = lastChapterDataJson['data'][0]['attributes']['chapter']; + completedVolumes = null; + + if ((manga.mediaListEntry || { progress: 0 }).progress > lastChapter && !disableGuessing) { + const anilistData = await recentMediaActivities( + identity, + manga, + settings.get().calculateGuessMethod + ); + + if (anilistData !== null && anilistData > lastChapter) lastChapter = anilistData; + } + + if (!settings.get().calculateDisableOutOfDateVolumeWarning) { + const volumeOfChapterData = await ( + await fetch( + proxy( + `https://api.mangadex.org/chapter?manga=${mangadexId}&chapter=${manga.mediaListEntry?.progress}&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&contentRating[]=pornographic&limit=1` + ) + ) + ).json(); + let lastAvailableVolume = lastChapterDataJson['data'][0]['attributes']['volume']; + + if (lastAvailableVolume === null) { + let chapterIndex = 0; + + while (chapterIndex < lastChapterDataJson['data'].length && lastAvailableVolume === null) { + if (lastChapterDataJson['data'][chapterIndex]['attributes']['volume'] !== null) + lastAvailableVolume = lastChapterDataJson['data'][chapterIndex]['attributes']['volume']; + + chapterIndex += 1; + } + } + + if (volumeOfChapterData['data'] !== undefined && volumeOfChapterData['data'].length > 0) { + const volumeOfChapter = volumeOfChapterData['data'][0]['attributes']['volume']; + + if (volumeOfChapter !== null) completedVolumes = volumeOfChapter; + + if (completedVolumes === volumeOfChapter) completedVolumes -= 1; + } + } + } else { + lastChapter = (await getChapterCount(manga.title.native)) || 0; + } + + if (lastChapter == 0) lastChapter = -1; + + await database.chapters.put({ + id: manga.id, + chapters: Number(lastChapter), + volumes: completedVolumes + }); + + return Number(lastChapter); }; diff --git a/src/lib/Media/Manga/volumes.ts b/src/lib/Media/Manga/volumes.ts index d6fda96d..05ae571d 100644 --- a/src/lib/Media/Manga/volumes.ts +++ b/src/lib/Media/Manga/volumes.ts @@ -2,4 +2,4 @@ import type { Media } from '$lib/Data/AniList/media'; import { database } from '../../Database/IDB/chapters'; export const volumeCount = async (manga: Media): Promise<number | null> => - (await database.chapters.get(manga.id))?.volumes as number | null; + (await database.chapters.get(manga.id))?.volumes as number | null; diff --git a/src/lib/Media/links.ts b/src/lib/Media/links.ts index 85b696d9..626e892a 100644 --- a/src/lib/Media/links.ts +++ b/src/lib/Media/links.ts @@ -2,50 +2,50 @@ import type { Media } from '$lib/Data/AniList/media'; import type { PrequelRelationNode } from '$lib/Data/AniList/prequels'; export const outboundLink = ( - media: Media | PrequelRelationNode | null, - type: 'anime' | 'manga', - setting: 'anilist' | 'livechartme' | 'animeschedule' | 'myanimelist', - search = false, - title: string | null = null + media: Media | PrequelRelationNode | null, + type: 'anime' | 'manga', + setting: 'anilist' | 'livechartme' | 'animeschedule' | 'myanimelist', + search = false, + title: string | null = null ) => { - media = media as Media; + media = media as Media; - if (type === 'manga') - switch (setting) { - case 'livechartme': - case 'animeschedule': - return `https://anilist.co/${type}/${media.id}/`; - case 'myanimelist': - return media.idMal - ? `https://myanimelist.net/manga/${media.idMal}` - : `https://myanimelist.net/manga.php?q=${encodeURIComponent( - media.title.native || media.title.english || media.title.romaji - )}&cat=manga`; - default: - return `https://anilist.co/${type}/${media.id}/`; - } - else - switch (setting) { - case 'anilist': - return search - ? `https://anilist.co/search?search=${encodeURIComponent(title || '')}` - : `https://anilist.co/${type}/${media.id}/`; - case 'livechartme': - return `https://www.livechart.me/search?q=${encodeURIComponent( - title || media.title.native || media.title.english || media.title.romaji - )}`; - case 'animeschedule': - return `https://animeschedule.net/shows?q=${encodeURIComponent( - title || media.title.native || media.title.english || media.title.romaji - )}`; - case 'myanimelist': { - return search - ? `https://myanimelist.net/anime.php?q=${title}&cat=anime` - : media.idMal - ? `https://myanimelist.net/anime/${media.idMal}` - : `https://myanimelist.net/anime.php?q=${encodeURIComponent( - media.title.native || media.title.english || media.title.romaji - )}&cat=anime`; - } - } + if (type === 'manga') + switch (setting) { + case 'livechartme': + case 'animeschedule': + return `https://anilist.co/${type}/${media.id}/`; + case 'myanimelist': + return media.idMal + ? `https://myanimelist.net/manga/${media.idMal}` + : `https://myanimelist.net/manga.php?q=${encodeURIComponent( + media.title.native || media.title.english || media.title.romaji + )}&cat=manga`; + default: + return `https://anilist.co/${type}/${media.id}/`; + } + else + switch (setting) { + case 'anilist': + return search + ? `https://anilist.co/search?search=${encodeURIComponent(title || '')}` + : `https://anilist.co/${type}/${media.id}/`; + case 'livechartme': + return `https://www.livechart.me/search?q=${encodeURIComponent( + title || media.title.native || media.title.english || media.title.romaji + )}`; + case 'animeschedule': + return `https://animeschedule.net/shows?q=${encodeURIComponent( + title || media.title.native || media.title.english || media.title.romaji + )}`; + case 'myanimelist': { + return search + ? `https://myanimelist.net/anime.php?q=${title}&cat=anime` + : media.idMal + ? `https://myanimelist.net/anime/${media.idMal}` + : `https://myanimelist.net/anime.php?q=${encodeURIComponent( + media.title.native || media.title.english || media.title.romaji + )}&cat=anime`; + } + } }; |