import { DOMPARSER } from './util.js' import { anilistClient } from './anilist.js' import _anitomyscript from 'anitomyscript' import { toast } from 'svelte-sonner' import SectionsManager from './sections.js' import { page } from '@/App.svelte' import clipboard from './clipboard.js' import { search, key } from '@/views/Search.svelte' import { playAnime } from '@/views/TorrentSearch/TorrentModal.svelte' import Helper from "@/modules/helper.js" const imageRx = /\.(jpeg|jpg|gif|png|webp)/i clipboard.on('files', ({ detail }) => { for (const file of detail) { if (file.type.startsWith('image')) { toast.promise(traceAnime(file), { description: 'You can also paste an URL to an image.', loading: 'Looking up anime for image...', success: 'Found anime for image!', error: 'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.' }) } } }) clipboard.on('text', ({ detail }) => { for (const { type, text } of detail) { let src = null if (type === 'text/html') { src = DOMPARSER(text, 'text/html').querySelectorAll('img')[0]?.src } else if (imageRx.exec(text)) { src = text } if (src) { toast.promise(traceAnime(src), { description: 'You can also paste an URL to an image.', loading: 'Looking up anime for image...', success: 'Found anime for image!', error: 'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.' }) } } }) export async function traceAnime (image) { // WAIT lookup logic let options let url = `https://api.trace.moe/search?cutBorders&url=${image}` if (image instanceof Blob) { options = { method: 'POST', body: image, headers: { 'Content-type': image.type } } url = 'https://api.trace.moe/search' } const res = await fetch(url, options) const { result } = await res.json() if (result?.length) { const ids = result.map(({ anilist }) => anilist) search.value = { clearNext: true, load: (page = 1, perPage = 50, variables = {}) => { const res = anilistClient.searchIDS({ page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) }).then(res => { for (const index in res.data?.Page?.media) { const media = res.data.Page.media[index] const counterpart = result.find(({ anilist }) => anilist === media.id) res.data.Page.media[index] = { media, episode: counterpart.episode, similarity: counterpart.similarity, episodeData: { image: counterpart.image, video: counterpart.video } } } res.data?.Page?.media.sort((a, b) => b.similarity - a.similarity) return res }) return SectionsManager.wrapResponse(res, result.length, 'episode') } } key.value = {} page.value = 'search' } else { throw new Error('Search Failed \n Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.') } } function constructChapters (results, duration) { const chapters = results.map(result => { const diff = duration - result.episodeLength return { start: (result.interval.startTime + diff) * 1000, end: (result.interval.endTime + diff) * 1000, text: result.skipType.toUpperCase() } }) const ed = chapters.find(({ text }) => text === 'ED') const recap = chapters.find(({ text }) => text === 'RECAP') if (recap) recap.text = 'Recap' chapters.sort((a, b) => a - b) if ((chapters[0].start | 0) !== 0) { chapters.unshift({ start: 0, end: chapters[0].start, text: chapters[0].text === 'OP' ? 'Intro' : 'Episode' }) } if (ed) { if ((ed.end | 0) + 5000 - duration * 1000 < 0) { chapters.push({ start: ed.end, end: duration * 1000, text: 'Preview' }) } } else if ((chapters[chapters.length - 1].end | 0) + 5000 - duration * 1000 < 0) { chapters.push({ start: chapters[chapters.length - 1].end, end: duration * 1000, text: 'Episode' }) } for (let i = 0, len = chapters.length - 2; i <= len; ++i) { const current = chapters[i] const next = chapters[i + 1] if ((current.end | 0) !== (next.start | 0)) { chapters.push({ start: current.end, end: next.start, text: 'Episode' }) } } chapters.sort((a, b) => a.start - b.start) return chapters } export async function getChaptersAniSkip (file, duration) { const resAccurate = await fetch(`https://api.aniskip.com/v2/skip-times/${file.media.media.idMal}/${file.media.episode}/?episodeLength=${duration}&types=op&types=ed&types=recap`) const jsonAccurate = await resAccurate.json() const resRough = await fetch(`https://api.aniskip.com/v2/skip-times/${file.media.media.idMal}/${file.media.episode}/?episodeLength=0&types=op&types=ed&types=recap`) const jsonRough = await resRough.json() const map = {} for (const result of [...jsonAccurate.results, ...jsonRough.results]) { map[result.skipType] ||= result } const results = Object.values(map) if (!results.length) return [] return constructChapters(results, duration) } export function getMediaMaxEp (media, playable) { if (playable) { return media.nextAiringEpisode?.episode - 1 || media.airingSchedule?.nodes?.[0]?.episode - 1 || media.episodes } else { return media.episodes || media.nextAiringEpisode?.episode - 1 || media.airingSchedule?.nodes?.[0]?.episode - 1 } } // utility method for correcting anitomyscript woes for what's needed export async function anitomyscript (...args) { // @ts-ignore const res = await _anitomyscript(...args) const parseObjs = Array.isArray(res) ? res : [res] for (const obj of parseObjs) { obj.anime_title ??= '' const seasonMatch = obj.anime_title.match(/S(\d{2})E(\d{2})/) if (seasonMatch) { obj.anime_season = seasonMatch[1] obj.episode_number = seasonMatch[2] obj.anime_title = obj.anime_title.replace(/S(\d{2})E(\d{2})/, '') } else if (Array.isArray(obj.anime_season)) { obj.anime_season = obj.anime_season[0] } const yearMatch = obj.anime_title.match(/ (19[5-9]\d|20\d{2})/) if (yearMatch && Number(yearMatch[1]) <= (new Date().getUTCFullYear() + 1)) { obj.anime_year = yearMatch[1] obj.anime_title = obj.anime_title.replace(/ (19[5-9]\d|20\d{2})/, '') } if (Number(obj.anime_season) > 1) obj.anime_title += ' S' + obj.anime_season } return parseObjs } export const formatMap = { TV: 'TV Series', TV_SHORT: 'TV Short', MOVIE: 'Movie', SPECIAL: 'Special', OVA: 'OVA', ONA: 'ONA', MUSIC: 'Music', undefined: 'N/A', null: 'N/A' } export const statusColorMap = { CURRENT: 'rgb(61,180,242)', PLANNING: 'rgb(247,154,99)', COMPLETED: 'rgb(123,213,85)', PAUSED: 'rgb(250,122,122)', REPEATING: '#3baeea', DROPPED: 'rgb(232,93,117)' } export async function playMedia (media) { let ep = 1 if (media.mediaListEntry) { const { status, progress } = media.mediaListEntry if (progress) { if (status === 'COMPLETED') { await setStatus('REPEATING', { episode: 0 }, media) } else { ep = Math.min(getMediaMaxEp(media, true), progress + 1) } } } playAnime(media, ep, true) media = null } export function setStatus (status, other = {}, media) { const fuzzyDate = Helper.getFuzzyDate(media, status) const variables = { id: media.id, idMal: media.idMal, status, score: media.mediaListEntry?.score || 0, repeat: media.mediaListEntry?.repeat || 0, ...fuzzyDate, ...other } return Helper.entry(media, variables) } const episodeMetadataMap = {} export async function getEpisodeMetadataForMedia (media) { if (episodeMetadataMap[media.id]) return episodeMetadataMap[media.id] const res = await fetch('https://api.ani.zip/mappings?anilist_id=' + media.id) const { episodes } = await res.json() episodeMetadataMap[media.id] = episodes return episodes }