diff --git a/common/components/cards/EpisodeCard.svelte b/common/components/cards/EpisodeCard.svelte
index e0dfe5a..7e9cf17 100644
--- a/common/components/cards/EpisodeCard.svelte
+++ b/common/components/cards/EpisodeCard.svelte
@@ -21,7 +21,7 @@
preview = state
}
- const progress = liveAnimeEpisodeProgress(media.id, data.episode)
+ const progress = liveAnimeEpisodeProgress(media?.id, data?.episode)
diff --git a/common/components/cards/EpisodePreviewCard.svelte b/common/components/cards/EpisodePreviewCard.svelte
index f22aae5..ec5c725 100644
--- a/common/components/cards/EpisodePreviewCard.svelte
+++ b/common/components/cards/EpisodePreviewCard.svelte
@@ -9,7 +9,7 @@
const episodeThumbnail = ((!media?.mediaListEntry?.status || !(media.mediaListEntry.status === 'CURRENT' && media.mediaListEntry.progress < data.episode)) && data.episodeData?.image) || media?.bannerImage || media?.coverImage.extraLarge || ' '
let hide = true
- const progress = liveAnimeEpisodeProgress(media.id, data.episode)
+ const progress = liveAnimeEpisodeProgress(media?.id, data?.episode)
diff --git a/common/modules/anilist.js b/common/modules/anilist.js
index 6cefdf6..7586ee5 100644
--- a/common/modules/anilist.js
+++ b/common/modules/anilist.js
@@ -246,7 +246,7 @@ class AnilistClient {
/**
* @param {string} query
- * @param {object} variables
+ * @param {Record
} variables
*/
alRequest (query, variables) {
const options = {
@@ -256,7 +256,7 @@ class AnilistClient {
Accept: 'application/json'
},
body: JSON.stringify({
- query: query.replace(/\s/g, ''),
+ query: query.replace(/\s/g, '').replaceAll(' ', ' '),
variables: {
sort: 'TRENDING_DESC',
page: 1,
@@ -278,6 +278,59 @@ class AnilistClient {
return media.reduce((prev, curr) => prev.lavenshtein <= curr.lavenshtein ? prev : curr)
}
+ /**
+ * @param {{key: string, title: string, year?: string, isAdult: boolean}[]} flattenedTitles
+ **/
+ async alSearchCompound (flattenedTitles) {
+ /** @type {Record<`v${number}`, string>} */
+ const requestVariables = flattenedTitles.reduce((obj, { title }, i) => {
+ obj[`v${i}`] = title
+ return obj
+ }, {})
+
+ const queryVariables = flattenedTitles.map((_, i) => `$v${i}: String`).join(', ')
+ const fragmentQueries = flattenedTitles.map(({ year, isAdult }, i) => /* js */`
+ v${i}: Page(perPage: 10) {
+ media(type: ANIME, search: $v${i}, status_in: [RELEASING, FINISHED], isAdult: ${!!isAdult} ${year ? `, seasonYear: ${year}` : ''}) {
+ ...med
+ }
+ }`)
+
+ const query = /* js */`
+ query(${queryVariables}){
+ ${fragmentQueries}
+ }
+
+ fragment med on Media {
+ id,
+ title {
+ romaji,
+ english,
+ native
+ },
+ synonyms
+ }`
+
+ /**
+ * @type {import('./al.d.ts').Query>}
+ * @returns {Promise<[string, import('./al.d.ts').Media][]>}
+ * */
+ const res = await this.alRequest(query, requestVariables)
+
+ /** @type {Record} */
+ const searchResults = {}
+ for (const [variableName, { media }] of Object.entries(res.data)) {
+ if (!media.length) continue
+ const titleObject = flattenedTitles[Number(variableName.slice(1))]
+ if (searchResults[titleObject.key]) continue
+ searchResults[titleObject.key] = media.map(media => getDistanceFromTitle(media, titleObject.title)).reduce((prev, curr) => prev.lavenshtein <= curr.lavenshtein ? prev : curr).id
+ }
+
+ const ids = Object.values(searchResults)
+ const search = await this.searchIDS({ id: ids, perPage: 50 })
+ return Object.entries(searchResults).map(([filename, id]) => [filename, search.data.Page.media.find(media => media.id === id)])
+ }
+
async alEntry (filemedia) {
// check if values exist
if (filemedia.media && alToken) {
diff --git a/common/modules/animeresolver.js b/common/modules/animeresolver.js
index 2e6db1e..60d7924 100644
--- a/common/modules/animeresolver.js
+++ b/common/modules/animeresolver.js
@@ -1,7 +1,6 @@
import { toast } from 'svelte-sonner'
import { anilistClient } from './anilist.js'
import { anitomyscript } from './anime.js'
-import { PromiseBatch } from './util.js'
const postfix = {
1: 'st', 2: 'nd', 3: 'rd'
@@ -22,83 +21,79 @@ export default new class AnimeResolver {
}
/**
- * resolve anime name based on file name and store it
- * @param {import('anitomyscript').AnitomyResult} parseObject
+ * @param {string} title
+ * @returns {string[]}
*/
- async findAnimeByTitle (parseObject) {
- const name = parseObject.anime_title
- const variables = { name, perPage: 10, status: ['RELEASING', 'FINISHED'], sort: 'SEARCH_MATCH' }
- if (parseObject.anime_year) variables.year = parseObject.anime_year
+ alternativeTitles (title) {
+ const titles = []
- // inefficient but readable
-
- let media = null
- try {
- // change S2 into Season 2 or 2nd Season
- const match = variables.name.match(/ S(\d+)/)
- const oldname = variables.name
- if (match) {
- if (Number(match[1]) === 1) { // if this is S1, remove the " S1" or " S01"
- variables.name = variables.name.replace(/ S(\d+)/, '')
- media = await anilistClient.alSearch(variables)
- } else {
- variables.name = variables.name.replace(/ S(\d+)/, ` ${Number(match[1])}${postfix[Number(match[1])] || 'th'} Season`)
- media = await anilistClient.alSearch(variables)
- if (!media) {
- variables.name = oldname.replace(/ S(\d+)/, ` Season ${Number(match[1])}`)
- media = await anilistClient.alSearch(variables)
- }
- }
+ let modified = title
+ // preemptively change S2 into Season 2 or 2nd Season, otherwise this will have accuracy issues
+ const seasonMatch = title.match(/ S(\d+)/)
+ if (seasonMatch) {
+ if (Number(seasonMatch[1]) === 1) { // if this is S1, remove the " S1" or " S01"
+ modified = title.replace(/ S(\d+)/, '')
+ titles.push(modified)
} else {
- media = await anilistClient.alSearch(variables)
+ modified = title.replace(/ S(\d+)/, ` ${Number(seasonMatch[1])}${postfix[Number(seasonMatch[1])] || 'th'} Season`)
+ titles.push(modified)
+ titles.push(title.replace(/ S(\d+)/, ` Season ${Number(seasonMatch[1])}`))
}
+ } else {
+ titles.push(title)
+ }
- // remove - :
- if (!media) {
- const match = variables.name.match(/[-:]/g)
- if (match) {
- variables.name = variables.name.replace(/[-:]/g, '')
- media = await anilistClient.alSearch(variables)
- }
- }
- // remove (TV)
- if (!media) {
- const match = variables.name.match(/\(TV\)/)
- if (match) {
- variables.name = variables.name.replace('(TV)', '')
- media = await anilistClient.alSearch(variables)
- }
- }
- // check adult
- if (!media) {
- variables.isAdult = true
- media = await anilistClient.alSearch(variables)
- }
- } catch (e) { }
+ // remove - :
+ const specialMatch = modified.match(/[-:]/g)
+ if (specialMatch) {
+ modified = modified.replace(/[-:]/g, '')
+ titles.push(modified)
+ }
- if (media) this.animeNameCache[this.getCacheKeyForTitle(parseObject)] = media
+ // remove (TV)
+ const tvMatch = modified.match(/\(TV\)/)
+ if (tvMatch) {
+ modified = modified.replace('(TV)', '')
+ titles.push(modified)
+ }
+
+ return titles
}
- // id keyed cache for anilist media
- animeCache = {}
+ /**
+ * resolve anime name based on file name and store it
+ * @param {import('anitomyscript').AnitomyResult[]} parseObjects
+ */
+ async findAnimesByTitle (parseObjects) {
+ const titleObjects = parseObjects.map(obj => {
+ const key = this.getCacheKeyForTitle(obj)
+ const titleObjects = this.alternativeTitles(obj.anime_title).map(title => ({ title, year: obj.anime_year, key, isAdult: false }))
+ titleObjects.push({ ...titleObjects.at(-1), isAdult: true })
+ return titleObjects
+ }).flat()
+
+ for (const [key, media] of await anilistClient.alSearchCompound(titleObjects)) {
+ this.animeNameCache[key] = media
+ }
+ }
- // 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] = anilistClient.searchIDSingle({ id })
+ async getAnimeById (id) {
+ if (anilistClient.mediaCache[id]) return anilistClient.mediaCache[id]
+ const res = await anilistClient.searchIDSingle({ id })
- return this.animeCache[id]
+ return res.data.Media
}
- // TODO: anidb aka true episodes need to be mapped to anilist episodes a bit better
+ // TODO: anidb aka true episodes need to be mapped to anilist episodes a bit better, shit like mushoku offsets caused by episode 0's in between seasons
/**
* @param {string | string[]} fileName
* @returns {Promise}
*/
async resolveFileAnime (fileName) {
+ if (!fileName) return [{}]
const parseObjs = await anitomyscript(fileName)
// batches promises in 10 at a time, because of CF burst protection, which still sometimes gets triggered :/
@@ -108,7 +103,7 @@ export default new class AnimeResolver {
if (key in this.animeNameCache) continue
uniq[key] = obj
}
- await PromiseBatch(this.findAnimeByTitle.bind(this), Object.values(uniq), 10)
+ await this.findAnimesByTitle(Object.values(uniq))
const fileAnimes = []
for (const parseObj of parseObjs) {
@@ -131,7 +126,7 @@ export default new class AnimeResolver {
// 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
+ const root = prequel && (await this.resolveSeason({ media: await this.getAnimeById(prequel.id), 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 })
@@ -148,7 +143,7 @@ export default new class AnimeResolver {
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
+ const root = prequel && (await this.resolveSeason({ media: await this.getAnimeById(prequel.id), 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 })
@@ -212,7 +207,7 @@ export default new class AnimeResolver {
}
return obj
}
- media = (await this.getAnimeById(edge.id)).data.Media
+ media = await this.getAnimeById(edge.id)
const highest = media.nextAiringEpisode?.episode || media.episodes
diff --git a/common/modules/extensions/worker.js b/common/modules/extensions/worker.js
index 4c94e4f..f9c70f9 100644
--- a/common/modules/extensions/worker.js
+++ b/common/modules/extensions/worker.js
@@ -38,7 +38,6 @@ class Extensions {
/** @param {string[]} extensions */
export async function loadExtensions (extensions) {
- // TODO: handle import errors
const sources = (await Promise.all(extensions.map(async extension => {
try {
if (!extension.startsWith('http')) extension = `https://esm.sh/${extension}`
diff --git a/common/modules/rss.js b/common/modules/rss.js
index 75d3729..0650d3c 100644
--- a/common/modules/rss.js
+++ b/common/modules/rss.js
@@ -62,7 +62,6 @@ export async function getRSSContent (url) {
class RSSMediaManager {
constructor () {
this.resultMap = {}
- this.lastResult = null
}
getMediaForRSS (page, perPage, url, ignoreErrors) {
@@ -101,7 +100,7 @@ class RSSMediaManager {
const targetPage = [...changed.content.querySelectorAll('item')].slice(index, index + perPage)
const items = parseRSSNodes(targetPage)
hasNextPage.value = items.length === perPage
- const result = items.map(item => this.resolveAnimeFromRSSItem(item))
+ const result = this.structureResolveResults(items)
this.resultMap[url] = {
date: changed.pubDate,
result
@@ -109,29 +108,26 @@ class RSSMediaManager {
return result
}
- resolveAnimeFromRSSItem (item) {
- this.lastResult = this.queueResolve(item)
- return this.lastResult
- }
-
- async queueResolve ({ title, link, date }) {
- await this.lastResult
- const res = {
- ...(await AnimeResolver.resolveFileAnime(title))[0],
- episodeData: undefined,
- date: undefined,
- onclick: undefined
- }
- if (res.media?.id) {
- try {
- res.episodeData = (await getEpisodeMetadataForMedia(res.media))?.[res.episode]
- } catch (e) {
- console.warn('failed fetching episode metadata', e)
+ async structureResolveResults (items) {
+ const results = await AnimeResolver.resolveFileAnime(items.map(item => item.title))
+ return results.map(async (result, i) => {
+ const res = {
+ ...result,
+ episodeData: undefined,
+ date: undefined,
+ onclick: undefined
}
- }
- res.date = date
- res.onclick = () => add(link)
- return res
+ if (res.media?.id) {
+ try {
+ res.episodeData = (await getEpisodeMetadataForMedia(res.media))?.[res.episode]
+ } catch (e) {
+ console.warn('failed fetching episode metadata', e)
+ }
+ }
+ res.date = items[i].date
+ res.onclick = () => add(items[i].link)
+ return res
+ })
}
}
diff --git a/common/modules/sections.js b/common/modules/sections.js
index efc91c3..6b932bb 100644
--- a/common/modules/sections.js
+++ b/common/modules/sections.js
@@ -65,15 +65,15 @@ function createSections () {
...settings.value.rssFeedsNew.map(([title, url]) => {
const section = {
title,
- load: (page = 1, perPage = 8) => RSSManager.getMediaForRSS(page, perPage, url),
- preview: writable(RSSManager.getMediaForRSS(1, 8, url)),
+ load: (page = 1, perPage = 12) => RSSManager.getMediaForRSS(page, perPage, url),
+ preview: writable(RSSManager.getMediaForRSS(1, 12, url)),
variables: { disableSearch: true }
}
// update every 30 seconds
section.interval = setInterval(async () => {
- if (await RSSManager.getContentChanged(1, 8, url)) {
- section.preview.value = RSSManager.getMediaForRSS(1, 8, url, true)
+ if (await RSSManager.getContentChanged(1, 12, url)) {
+ section.preview.value = RSSManager.getMediaForRSS(1, 12, url, true)
}
}, 30000)
diff --git a/common/modules/util.js b/common/modules/util.js
index 56685d0..b57e7f2 100644
--- a/common/modules/util.js
+++ b/common/modules/util.js
@@ -77,18 +77,6 @@ export function toTS (sec, full) {
if (seconds < 10) seconds = '0' + seconds
return (hours > 0 || full === 1 || full === 2) ? hours + ':' + minutes + ':' + seconds : minutes + ':' + seconds
}
-
-export async function PromiseBatch (task, items, batchSize) {
- let position = 0
- let results = []
- while (position < items.length) {
- const itemsForBatch = items.slice(position, position + batchSize)
- results = [...results, ...await Promise.all(itemsForBatch.map(item => task(item)))]
- position += batchSize
- }
- return results
-}
-
export function generateRandomHexCode (len) {
let hexCode = ''
@@ -126,25 +114,6 @@ export function debounce (fn, time) {
}
}
-export function binarySearch (arr, el) {
- let left = 0
- let right = arr.length - 1
-
- while (left <= right) {
- // Using bitwise or instead of Math.floor as it is slightly faster
- const mid = ((right + left) / 2) | 0
- if (arr[mid] === el) {
- return true
- } else if (el < arr[mid]) {
- right = mid - 1
- } else {
- left = mid + 1
- }
- }
-
- return false
-}
-
export const defaults = {
volume: 1,
playerAutoplay: true,
diff --git a/common/webpack.config.cjs b/common/webpack.config.cjs
index 47be4f7..3c9cd93 100644
--- a/common/webpack.config.cjs
+++ b/common/webpack.config.cjs
@@ -83,7 +83,7 @@ module.exports = (parentDir, alias = {}, aliasFields = 'browser', filename = 'ap
Miru
-
+
${htmlWebpackPlugin.tags.headTags}
diff --git a/electron/package.json b/electron/package.json
index 624e633..c75f2b4 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -1,6 +1,6 @@
{
"name": "Miru",
- "version": "5.0.0",
+ "version": "5.0.1",
"private": true,
"author": "ThaUnknown_ ",
"description": "Stream anime torrents, real-time with no waiting for downloads.",
diff --git a/electron/src/main/util.js b/electron/src/main/util.js
index 72a0ecc..24782cd 100644
--- a/electron/src/main/util.js
+++ b/electron/src/main/util.js
@@ -1,4 +1,4 @@
-import { app, ipcMain, shell, dialog } from 'electron'
+import { app, ipcMain, shell } from 'electron'
import store from './store.js'
export const development = process.env.NODE_ENV?.trim() === 'development'
@@ -14,10 +14,8 @@ const flags = [
['enable-features', 'PlatformEncryptedDolbyVision,EnableDrDc,CanvasOopRasterization,ThrottleDisplayNoneAndVisibilityHiddenCrossOriginIframes,UseSkiaRenderer,WebAssemblyLazyCompilation'],
['force_high_performance_gpu'],
['disable-features', 'Vulkan,CalculateNativeWinOcclusion,WidgetLayering'],
- ['disable-color-correct-rendering'],
['autoplay-policy', 'no-user-gesture-required'], ['disable-notifications'], ['disable-logging'], ['disable-permissions-api'], ['no-sandbox'], ['no-zygote'],
- ['bypasscsp-schemes'],
- ['force-color-profile', 'srgb'] // TODO: should this be "scrgb-linear"?
+ ['bypasscsp-schemes']
]
for (const [flag, value] of flags) {
app.commandLine.appendSwitch(flag, value)