mirror of
https://github.com/NoCrypt/migu.git
synced 2026-04-19 15:42:05 +00:00
feat: anime resolve cache
This commit is contained in:
parent
941abeeeff
commit
1bb771992f
4 changed files with 238 additions and 202 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { DOMPARSER, PromiseBatch } from './util.js'
|
||||
import { alRequest, alSearch } from './anilist.js'
|
||||
import { DOMPARSER } from './util.js'
|
||||
import { alRequest } from './anilist.js'
|
||||
import _anitomyscript from 'anitomyscript'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import SectionsManager from './sections.js'
|
||||
|
|
@ -161,67 +161,6 @@ export function getMediaMaxEp (media, playable) {
|
|||
}
|
||||
}
|
||||
|
||||
// resolve anime name based on file name and store it
|
||||
const postfix = {
|
||||
1: 'st',
|
||||
2: 'nd',
|
||||
3: 'rd'
|
||||
}
|
||||
|
||||
async function resolveTitle (parseObject) {
|
||||
const name = parseObject.anime_title
|
||||
const method = { name, method: 'SearchName', perPage: 10, status: ['RELEASING', 'FINISHED'], sort: 'SEARCH_MATCH' }
|
||||
if (parseObject.anime_year) method.year = parseObject.anime_year
|
||||
|
||||
// inefficient but readable
|
||||
|
||||
let media = null
|
||||
try {
|
||||
// change S2 into Season 2 or 2nd Season
|
||||
const match = method.name.match(/ S(\d+)/)
|
||||
const oldname = method.name
|
||||
if (match) {
|
||||
if (Number(match[1]) === 1) { // if this is S1, remove the " S1" or " S01"
|
||||
method.name = method.name.replace(/ S(\d+)/, '')
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
} else {
|
||||
method.name = method.name.replace(/ S(\d+)/, ` ${Number(match[1])}${postfix[Number(match[1])] || 'th'} Season`)
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
if (!media) {
|
||||
method.name = oldname.replace(/ S(\d+)/, ` Season ${Number(match[1])}`)
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
}
|
||||
|
||||
// remove - :
|
||||
if (!media) {
|
||||
const match = method.name.match(/[-:]/g)
|
||||
if (match) {
|
||||
method.name = method.name.replace(/[-:]/g, '')
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
}
|
||||
}
|
||||
// remove (TV)
|
||||
if (!media) {
|
||||
const match = method.name.match(/\(TV\)/)
|
||||
if (match) {
|
||||
method.name = method.name.replace('(TV)', '')
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
}
|
||||
}
|
||||
// check adult
|
||||
if (!media) {
|
||||
method.isAdult = true
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
if (media) relations[getRelationKey(parseObject)] = media
|
||||
}
|
||||
|
||||
// utility method for correcting anitomyscript woes for what's needed
|
||||
export async function anitomyscript (...args) {
|
||||
// @ts-ignore
|
||||
|
|
@ -248,141 +187,6 @@ export async function anitomyscript (...args) {
|
|||
return parseObjs
|
||||
}
|
||||
|
||||
function getRelationKey (obj) {
|
||||
let key = obj.anime_title
|
||||
if (obj.anime_year) key += obj.anime_year
|
||||
return key
|
||||
}
|
||||
|
||||
// TODO: anidb aka true episodes need to be mapped to anilist episodes a bit better
|
||||
export async function resolveFileMedia (fileName) {
|
||||
const parseObjs = await anitomyscript(fileName)
|
||||
|
||||
// batches promises in 10 at a time, because of CF burst protection, which still sometimes gets triggered :/
|
||||
const uniq = {}
|
||||
for (const obj of parseObjs) {
|
||||
const key = getRelationKey(obj)
|
||||
if (key in relations) continue
|
||||
uniq[key] = obj
|
||||
}
|
||||
await PromiseBatch(resolveTitle, Object.values(uniq), 10)
|
||||
|
||||
const fileMedias = []
|
||||
for (const parseObj of parseObjs) {
|
||||
let failed = false
|
||||
let episode
|
||||
let media = relations[getRelationKey(parseObj)]
|
||||
// resolve episode, if movie, dont.
|
||||
const maxep = media?.nextAiringEpisode?.episode || media?.episodes
|
||||
if ((media?.format !== 'MOVIE' || maxep) && parseObj.episode_number) {
|
||||
if (Array.isArray(parseObj.episode_number)) {
|
||||
// is an episode range
|
||||
if (parseInt(parseObj.episode_number[0]) === 1) {
|
||||
// if it starts with #1 and overflows then it includes more than 1 season in a batch, cant fix this cleanly, name is parsed per file basis so this shouldnt be an issue
|
||||
episode = `${parseObj.episode_number[0]} ~ ${parseObj.episode_number[1]}`
|
||||
} else {
|
||||
if (maxep && parseInt(parseObj.episode_number[1]) > maxep) {
|
||||
// get root media to start at S1, instead of S2 or some OVA due to parsing errors
|
||||
// this is most likely safe, if it was relative episodes then it would likely use an accurate title for the season
|
||||
// if they didnt use an accurate title then its likely an absolute numbering scheme
|
||||
// parent check is to break out of those incorrectly resolved OVA's
|
||||
// if we used anime season to resolve anime name, then there's no need to march into prequel!
|
||||
const prequel = !parseObj.anime_season && (findEdge(media, 'PREQUEL')?.node || ((media.format === 'OVA' || media.format === 'ONA') && findEdge(media, 'PARENT')?.node))
|
||||
const root = prequel && (await resolveSeason({ media: (await alRequest({ method: 'SearchIDSingle', id: prequel.id })).data.Media, force: true })).media
|
||||
|
||||
// if highest value is bigger than episode count or latest streamed episode +1 for safety, parseint to math.floor a number like 12.5 - specials - in 1 go
|
||||
const result = await resolveSeason({ media: root || media, episode: parseObj.episode_number[1], increment: !parseObj.anime_season ? null : true })
|
||||
media = result.rootMedia
|
||||
const diff = parseObj.episode_number[1] - result.episode
|
||||
episode = `${parseObj.episode_number[0] - diff} ~ ${result.episode}`
|
||||
failed = result.failed
|
||||
} else {
|
||||
// cant find ep count or range seems fine
|
||||
episode = `${Number(parseObj.episode_number[0])} ~ ${Number(parseObj.episode_number[1])}`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (maxep && parseInt(parseObj.episode_number) > maxep) {
|
||||
// see big comment above
|
||||
const prequel = !parseObj.anime_season && (findEdge(media, 'PREQUEL')?.node || ((media.format === 'OVA' || media.format === 'ONA') && findEdge(media, 'PARENT')?.node))
|
||||
const root = prequel && (await resolveSeason({ media: (await alRequest({ method: 'SearchIDSingle', id: prequel.id })).data.Media, force: true })).media
|
||||
|
||||
// value bigger than episode count
|
||||
const result = await resolveSeason({ media: root || media, episode: parseInt(parseObj.episode_number), increment: !parseObj.anime_season ? null : true })
|
||||
media = result.rootMedia
|
||||
episode = result.episode
|
||||
failed = result.failed
|
||||
} else {
|
||||
// cant find ep count or episode seems fine
|
||||
episode = Number(parseObj.episode_number)
|
||||
}
|
||||
}
|
||||
}
|
||||
fileMedias.push({
|
||||
episode: episode || parseObj.episode_number,
|
||||
parseObject: parseObj,
|
||||
media,
|
||||
failed
|
||||
})
|
||||
}
|
||||
return fileMedias
|
||||
}
|
||||
|
||||
export function findEdge (media, type, formats = ['TV', 'TV_SHORT'], skip) {
|
||||
let res = media.relations.edges.find(edge => {
|
||||
if (edge.relationType === type) {
|
||||
return formats.includes(edge.node.format)
|
||||
}
|
||||
return false
|
||||
})
|
||||
// this is hit-miss
|
||||
if (!res && !skip && type === 'SEQUEL') res = findEdge(media, type, formats = ['TV', 'TV_SHORT', 'OVA'], true)
|
||||
return res
|
||||
}
|
||||
|
||||
// note: this doesnt cover anime which uses partially relative and partially absolute episode number, BUT IT COULD!
|
||||
export async function resolveSeason (opts) {
|
||||
// media, episode, increment, offset, force
|
||||
if (!opts.media || !(opts.episode || opts.force)) throw new Error('No episode or media for season resolve!')
|
||||
|
||||
let { media, episode, increment, offset = 0, rootMedia = opts.media, force } = opts
|
||||
|
||||
const rootHighest = (rootMedia.nextAiringEpisode?.episode || rootMedia.episodes)
|
||||
|
||||
const prequel = !increment && findEdge(media, 'PREQUEL')?.node
|
||||
const sequel = !prequel && (increment || increment == null) && findEdge(media, 'SEQUEL')?.node
|
||||
const edge = prequel || sequel
|
||||
increment = increment ?? !prequel
|
||||
|
||||
if (!edge) {
|
||||
const obj = { media, episode: episode - offset, offset, increment, rootMedia, failed: true }
|
||||
if (!force) {
|
||||
console.warn('Error in parsing!', obj)
|
||||
toast('Parsing Error', {
|
||||
description: `Failed resolving anime episode!\n${media.title.userPreferred} - ${episode - offset}`
|
||||
})
|
||||
}
|
||||
return obj
|
||||
}
|
||||
media = (await alRequest({ method: 'SearchIDSingle', id: edge.id })).data.Media
|
||||
|
||||
const highest = media.nextAiringEpisode?.episode || media.episodes
|
||||
|
||||
const diff = episode - (highest + offset)
|
||||
offset += increment ? rootHighest : highest
|
||||
if (increment) rootMedia = media
|
||||
|
||||
// force marches till end of tree, no need for checks
|
||||
if (!force && diff <= rootHighest) {
|
||||
episode -= offset
|
||||
return { media, episode, offset, increment, rootMedia }
|
||||
}
|
||||
|
||||
return resolveSeason({ media, episode, increment, offset, rootMedia, force })
|
||||
}
|
||||
|
||||
const relations = {}
|
||||
|
||||
export const formatMap = {
|
||||
TV: 'TV Series',
|
||||
TV_SHORT: 'TV Short',
|
||||
|
|
|
|||
231
common/modules/animeresolver.js
Normal file
231
common/modules/animeresolver.js
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import { toast } from 'svelte-sonner'
|
||||
import { alRequest, alSearch } from './anilist.js'
|
||||
import { anitomyscript } from './anime.js'
|
||||
import { PromiseBatch } from './util.js'
|
||||
|
||||
const postfix = {
|
||||
1: 'st', 2: 'nd', 3: 'rd'
|
||||
}
|
||||
|
||||
export default new class AnimeResolver {
|
||||
// name: media cache from title resolving
|
||||
animeNameCache = {}
|
||||
|
||||
/**
|
||||
* @param {import('anitomyscript').AnitomyResult} obj
|
||||
* @returns {string}
|
||||
*/
|
||||
getCacheKeyForTitle (obj) {
|
||||
let key = obj.anime_title
|
||||
if (obj.anime_year) key += obj.anime_year
|
||||
return key
|
||||
}
|
||||
|
||||
/**
|
||||
* resolve anime name based on file name and store it
|
||||
* @param {import('anitomyscript').AnitomyResult} parseObject
|
||||
*/
|
||||
async findAnimeByTitle (parseObject) {
|
||||
const name = parseObject.anime_title
|
||||
const method = { name, method: 'SearchName', perPage: 10, status: ['RELEASING', 'FINISHED'], sort: 'SEARCH_MATCH' }
|
||||
if (parseObject.anime_year) method.year = parseObject.anime_year
|
||||
|
||||
// inefficient but readable
|
||||
|
||||
let media = null
|
||||
try {
|
||||
// change S2 into Season 2 or 2nd Season
|
||||
const match = method.name.match(/ S(\d+)/)
|
||||
const oldname = method.name
|
||||
if (match) {
|
||||
if (Number(match[1]) === 1) { // if this is S1, remove the " S1" or " S01"
|
||||
method.name = method.name.replace(/ S(\d+)/, '')
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
} else {
|
||||
method.name = method.name.replace(/ S(\d+)/, ` ${Number(match[1])}${postfix[Number(match[1])] || 'th'} Season`)
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
if (!media) {
|
||||
method.name = oldname.replace(/ S(\d+)/, ` Season ${Number(match[1])}`)
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
}
|
||||
|
||||
// remove - :
|
||||
if (!media) {
|
||||
const match = method.name.match(/[-:]/g)
|
||||
if (match) {
|
||||
method.name = method.name.replace(/[-:]/g, '')
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
}
|
||||
}
|
||||
// remove (TV)
|
||||
if (!media) {
|
||||
const match = method.name.match(/\(TV\)/)
|
||||
if (match) {
|
||||
method.name = method.name.replace('(TV)', '')
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
}
|
||||
}
|
||||
// check adult
|
||||
if (!media) {
|
||||
method.isAdult = true
|
||||
media = (await alSearch(method)).data.Page.media[0]
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
if (media) this.animeNameCache[this.getCacheKeyForTitle(parseObject)] = media
|
||||
}
|
||||
|
||||
// id keyed cache for anilist media
|
||||
animeCache = {}
|
||||
|
||||
// TODO: this should use global anime cache once that is create
|
||||
/**
|
||||
* @param {number} id
|
||||
* @returns {any}
|
||||
*/
|
||||
getAnimeById (id) {
|
||||
if (!this.animeCache[id]) this.animeCache[id] = alRequest({ method: 'SearchIDSingle', id })
|
||||
|
||||
return this.animeCache[id]
|
||||
}
|
||||
|
||||
// TODO: anidb aka true episodes need to be mapped to anilist episodes a bit better
|
||||
/**
|
||||
* @param {string | string[]} fileName
|
||||
* @returns {Promise<any[]>}
|
||||
*/
|
||||
async resolveFileAnime (fileName) {
|
||||
const parseObjs = await anitomyscript(fileName)
|
||||
|
||||
// batches promises in 10 at a time, because of CF burst protection, which still sometimes gets triggered :/
|
||||
const uniq = {}
|
||||
for (const obj of parseObjs) {
|
||||
const key = this.getCacheKeyForTitle(obj)
|
||||
if (key in this.animeNameCache) continue
|
||||
uniq[key] = obj
|
||||
}
|
||||
await PromiseBatch(this.findAnimeByTitle.bind(this), Object.values(uniq), 10)
|
||||
|
||||
const fileAnimes = []
|
||||
for (const parseObj of parseObjs) {
|
||||
let failed = false
|
||||
let episode
|
||||
let media = this.animeNameCache[this.getCacheKeyForTitle(parseObj)]
|
||||
// resolve episode, if movie, dont.
|
||||
const maxep = media?.nextAiringEpisode?.episode || media?.episodes
|
||||
if ((media?.format !== 'MOVIE' || maxep) && parseObj.episode_number) {
|
||||
if (Array.isArray(parseObj.episode_number)) {
|
||||
// is an episode range
|
||||
if (parseInt(parseObj.episode_number[0]) === 1) {
|
||||
// if it starts with #1 and overflows then it includes more than 1 season in a batch, cant fix this cleanly, name is parsed per file basis so this shouldnt be an issue
|
||||
episode = `${parseObj.episode_number[0]} ~ ${parseObj.episode_number[1]}`
|
||||
} else {
|
||||
if (maxep && parseInt(parseObj.episode_number[1]) > maxep) {
|
||||
// get root media to start at S1, instead of S2 or some OVA due to parsing errors
|
||||
// this is most likely safe, if it was relative episodes then it would likely use an accurate title for the season
|
||||
// if they didnt use an accurate title then its likely an absolute numbering scheme
|
||||
// parent check is to break out of those incorrectly resolved OVA's
|
||||
// if we used anime season to resolve anime name, then there's no need to march into prequel!
|
||||
const prequel = !parseObj.anime_season && (this.findEdge(media, 'PREQUEL')?.node || ((media.format === 'OVA' || media.format === 'ONA') && this.findEdge(media, 'PARENT')?.node))
|
||||
const root = prequel && (await this.resolveSeason({ media: (await this.getAnimeById(prequel.id)).data.Media, force: true })).media
|
||||
|
||||
// if highest value is bigger than episode count or latest streamed episode +1 for safety, parseint to math.floor a number like 12.5 - specials - in 1 go
|
||||
const result = await this.resolveSeason({ media: root || media, episode: parseObj.episode_number[1], increment: !parseObj.anime_season ? null : true })
|
||||
media = result.rootMedia
|
||||
const diff = parseObj.episode_number[1] - result.episode
|
||||
episode = `${parseObj.episode_number[0] - diff} ~ ${result.episode}`
|
||||
failed = result.failed
|
||||
} else {
|
||||
// cant find ep count or range seems fine
|
||||
episode = `${Number(parseObj.episode_number[0])} ~ ${Number(parseObj.episode_number[1])}`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (maxep && parseInt(parseObj.episode_number) > maxep) {
|
||||
// see big comment above
|
||||
const prequel = !parseObj.anime_season && (this.findEdge(media, 'PREQUEL')?.node || ((media.format === 'OVA' || media.format === 'ONA') && this.findEdge(media, 'PARENT')?.node))
|
||||
const root = prequel && (await this.resolveSeason({ media: (await this.getAnimeById(prequel.id)).data.Media, force: true })).media
|
||||
|
||||
// value bigger than episode count
|
||||
const result = await this.resolveSeason({ media: root || media, episode: parseInt(parseObj.episode_number), increment: !parseObj.anime_season ? null : true })
|
||||
media = result.rootMedia
|
||||
episode = result.episode
|
||||
failed = result.failed
|
||||
} else {
|
||||
// cant find ep count or episode seems fine
|
||||
episode = Number(parseObj.episode_number)
|
||||
}
|
||||
}
|
||||
}
|
||||
fileAnimes.push({
|
||||
episode: episode || parseObj.episode_number,
|
||||
parseObject: parseObj,
|
||||
media,
|
||||
failed
|
||||
})
|
||||
}
|
||||
return fileAnimes
|
||||
}
|
||||
|
||||
findEdge (media, type, formats = ['TV', 'TV_SHORT'], skip) {
|
||||
let res = media.relations.edges.find(edge => {
|
||||
if (edge.relationType === type) {
|
||||
return formats.includes(edge.node.format)
|
||||
}
|
||||
return false
|
||||
})
|
||||
// this is hit-miss
|
||||
if (!res && !skip && type === 'SEQUEL') res = this.findEdge(media, type, formats = ['TV', 'TV_SHORT', 'OVA'], true)
|
||||
return res
|
||||
}
|
||||
|
||||
// note: this doesnt cover anime which uses partially relative and partially absolute episode number, BUT IT COULD!
|
||||
/**
|
||||
*
|
||||
* @param {{ media:any, episode?:number, force?:boolean, increment?:boolean, offset?: number, rootMedia?:any }} opts
|
||||
* @returns
|
||||
*/
|
||||
async resolveSeason (opts) {
|
||||
// media, episode, increment, offset, force
|
||||
if (!opts.media || !(opts.episode || opts.force)) throw new Error('No episode or media for season resolve!')
|
||||
|
||||
let { media, episode, increment, offset = 0, rootMedia = opts.media, force } = opts
|
||||
|
||||
const rootHighest = (rootMedia.nextAiringEpisode?.episode || rootMedia.episodes)
|
||||
|
||||
const prequel = !increment && this.findEdge(media, 'PREQUEL')?.node
|
||||
const sequel = !prequel && (increment || increment == null) && this.findEdge(media, 'SEQUEL')?.node
|
||||
const edge = prequel || sequel
|
||||
increment = increment ?? !prequel
|
||||
|
||||
if (!edge) {
|
||||
const obj = { media, episode: episode - offset, offset, increment, rootMedia, failed: true }
|
||||
if (!force) {
|
||||
console.warn('Error in parsing!', obj)
|
||||
toast('Parsing Error', {
|
||||
description: `Failed resolving anime episode!\n${media.title.userPreferred} - ${episode - offset}`
|
||||
})
|
||||
}
|
||||
return obj
|
||||
}
|
||||
media = (await this.getAnimeById(edge.id)).data.Media
|
||||
|
||||
const highest = media.nextAiringEpisode?.episode || media.episodes
|
||||
|
||||
const diff = episode - (highest + offset)
|
||||
offset += increment ? rootHighest : highest
|
||||
if (increment) rootMedia = media
|
||||
|
||||
// force marches till end of tree, no need for checks
|
||||
if (!force && diff <= rootHighest) {
|
||||
episode -= offset
|
||||
return { media, episode, offset, increment, rootMedia }
|
||||
}
|
||||
|
||||
return this.resolveSeason({ media, episode, increment, offset, rootMedia, force })
|
||||
}
|
||||
}()
|
||||
|
|
@ -2,7 +2,8 @@ import { DOMPARSER } from '@/modules/util.js'
|
|||
import { settings } from '@/modules/settings.js'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import { add } from '@/modules/torrent.js'
|
||||
import { resolveFileMedia, getEpisodeMetadataForMedia } from './anime.js'
|
||||
import { getEpisodeMetadataForMedia } from './anime.js'
|
||||
import AnimeResolver from '@/modules/animeresolver.js'
|
||||
import { hasNextPage } from '@/modules/sections.js'
|
||||
|
||||
export const exclusions = ['DTS', '[EMBER]']
|
||||
|
|
@ -116,7 +117,7 @@ class RSSMediaManager {
|
|||
async queueResolve ({ title, link, date }) {
|
||||
await this.lastResult
|
||||
const res = {
|
||||
...(await resolveFileMedia(title))[0],
|
||||
...(await AnimeResolver.resolveFileAnime(title))[0],
|
||||
episodeData: undefined,
|
||||
date: undefined,
|
||||
onclick: undefined
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script context='module'>
|
||||
import { writable } from 'simple-store-svelte'
|
||||
import { resolveFileMedia } from '@/modules/anime.js'
|
||||
import AnimeResolver from '@/modules/animeresolver.js'
|
||||
import { videoRx } from '@/modules/util.js'
|
||||
import { tick } from 'svelte'
|
||||
import { state } from '../WatchTogether/WatchTogether.svelte'
|
||||
|
|
@ -120,7 +120,7 @@
|
|||
}
|
||||
let nowPlaying = media.value
|
||||
|
||||
const resolved = await resolveFileMedia(videoFiles.map(file => file.name))
|
||||
const resolved = await AnimeResolver.resolveFileAnime(videoFiles.map(file => file.name))
|
||||
|
||||
videoFiles.map(file => {
|
||||
file.media = resolved.find(({ parseObject }) => file.name.includes(parseObject.file_name))
|
||||
|
|
|
|||
Loading…
Reference in a new issue