mirror of
https://github.com/NoCrypt/migu.git
synced 2026-03-11 17:45:32 +00:00
244 lines
7.6 KiB
JavaScript
244 lines
7.6 KiB
JavaScript
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'
|
|
|
|
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})/, '')
|
|
}
|
|
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 variables = {
|
|
id: media.id,
|
|
status,
|
|
...other
|
|
}
|
|
return anilistClient.entry(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
|
|
}
|