mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-20 12:02:05 +00:00
feat: improve tosho batch lookup
feat: infinite scroll on search fix: update data on list update
This commit is contained in:
parent
517352b7ce
commit
d954314e9b
8 changed files with 155 additions and 412 deletions
|
|
@ -31,7 +31,7 @@
|
|||
<Toasts />
|
||||
<div class='page-wrapper with-sidebar with-transitions bg-dark' data-sidebar-type='overlayed-all'>
|
||||
<div class='sticky-alerts' />
|
||||
<CatBlock />
|
||||
<!-- <CatBlock /> -->
|
||||
<Menubar bind:page={$page} />
|
||||
<ViewAnime />
|
||||
<ViewTrailer />
|
||||
|
|
|
|||
|
|
@ -166,14 +166,6 @@ function getDistanceFromTitle (media, name) {
|
|||
}
|
||||
}
|
||||
|
||||
function sanitiseObject (object = {}) {
|
||||
const safe = {}
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
if (value) safe[key] = value
|
||||
}
|
||||
return safe
|
||||
}
|
||||
|
||||
export async function alSearch (method) {
|
||||
const res = await alRequest(method)
|
||||
const media = res.data.Page.media.map(media => getDistanceFromTitle(media, method.name))
|
||||
|
|
@ -214,6 +206,11 @@ nextAiringEpisode{
|
|||
timeUntilAiring,
|
||||
episode
|
||||
},
|
||||
startDate{
|
||||
year,
|
||||
month,
|
||||
day
|
||||
},
|
||||
trailer{
|
||||
id,
|
||||
site
|
||||
|
|
@ -252,6 +249,9 @@ relations {
|
|||
status,
|
||||
format,
|
||||
episodes,
|
||||
synonyms,
|
||||
season,
|
||||
seasonYear,
|
||||
startDate{
|
||||
year,
|
||||
month,
|
||||
|
|
@ -284,7 +284,7 @@ recommendations{
|
|||
export async function alRequest (opts) {
|
||||
let query
|
||||
const variables = {
|
||||
...sanitiseObject(opts),
|
||||
...opts,
|
||||
sort: opts.sort || 'TRENDING_DESC',
|
||||
page: opts.page || 1,
|
||||
perPage: opts.perPage || 30,
|
||||
|
|
|
|||
|
|
@ -176,14 +176,6 @@ async function resolveTitle (name) {
|
|||
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]
|
||||
}
|
||||
}
|
||||
// remove - :
|
||||
if (!media) {
|
||||
const match = method.name.match(/[-:]/g)
|
||||
|
|
@ -192,6 +184,14 @@ async function resolveTitle (name) {
|
|||
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]
|
||||
}
|
||||
}
|
||||
// remove 2020
|
||||
if (!media) {
|
||||
const match = method.name.match(/ (19[5-9]\d|20\d{2})/)
|
||||
|
|
|
|||
|
|
@ -23,14 +23,19 @@ export default async function tosho ({ media, episode }) {
|
|||
window.tosho = tosho
|
||||
|
||||
async function getAniDBFromAL (media) {
|
||||
console.log('getting AniDB ID from AL')
|
||||
const mappingsResponse = await fetch('https://api.ani.zip/mappings?anilist_id=' + media.id)
|
||||
const json = await mappingsResponse.json()
|
||||
if (json.mappings.anidb_id) return json
|
||||
|
||||
console.log('failed getting AniDB ID, checking via parent')
|
||||
|
||||
const parentID = getParentForSpecial(media)
|
||||
|
||||
if (!parentID) return
|
||||
|
||||
console.log('found via parent')
|
||||
|
||||
const parentResponse = await fetch('https://api.ani.zip/mappings?anilist_id=' + parentID)
|
||||
return parentResponse.json()
|
||||
}
|
||||
|
|
@ -48,8 +53,10 @@ function getRelation (list, type) {
|
|||
|
||||
// TODO: https://anilist.co/anime/13055/
|
||||
async function getAniDBEpisodeFromAL ({ media, episode }, { episodes, episodeCount }) {
|
||||
console.log('getting AniDB EpID for Mal EP', { episode, episodes })
|
||||
if (!episode || !Object.values(episodes).length) return
|
||||
if (media.episodes && media.episodes === episodeCount && episodes[Number(episode)]) return episodes[Number(episode)]
|
||||
console.log('EP count doesn\'t match, checking by air date')
|
||||
const res = await alRequest({ method: 'EpisodeDate', id: media.id, ep: episode })
|
||||
const alDate = new Date((res.data.AiringSchedule?.airingAt || 0) * 1000)
|
||||
|
||||
|
|
@ -68,23 +75,106 @@ async function getToshoEntries (media, episode, { mappings }, quality) {
|
|||
if (episode) {
|
||||
const { anidbEid } = episode
|
||||
|
||||
console.log('fetching episode', anidbEid, quality)
|
||||
|
||||
promises.push(fetchSingleEpisode({ id: anidbEid, quality }))
|
||||
} else {
|
||||
// TODO: look for episodes via.... title?
|
||||
}
|
||||
|
||||
// look for batches and movies
|
||||
if (mappings.anidb_id && media.status === 'FINISHED' && (isMovie(media) || media.episodes !== 1)) {
|
||||
promises.push(fetchBatches({ media, id: mappings.anidb_id, quality }))
|
||||
const movie = isMovie(media)
|
||||
if (mappings.anidb_id && media.status === 'FINISHED' && (movie || media.episodes !== 1)) {
|
||||
promises.push(fetchBatches({ episodeCount: media.episodes, id: mappings.anidb_id, quality }))
|
||||
console.log('fetching batch', quality, movie)
|
||||
if (!movie) {
|
||||
const courRelation = getSplitCourRelation(media)
|
||||
if (courRelation) {
|
||||
console.log('found split cour!')
|
||||
const episodeCount = (media.episodes || 0) + (courRelation.episodes || 0)
|
||||
const mappingsResponse = await fetch('https://api.ani.zip/mappings?anilist_id=' + courRelation.id)
|
||||
const json = await mappingsResponse.json()
|
||||
console.log('found mappings for split cour', !!json.mappings.anidb_id)
|
||||
if (json.mappings.anidb_id) promises.push(fetchBatches({ episodeCount, id: json.mappings.anidb_id, quality }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (await Promise.all(promises)).flat()
|
||||
}
|
||||
|
||||
function getSplitCourRelation (media) {
|
||||
// Part 2 / Cour 3 / 4th Cour
|
||||
if (isTitleSplitCour(media)) return getCourPrequel(media)
|
||||
|
||||
// Part 1 of split cour which usually doesn't get labeled as split cour
|
||||
// sequel can not exist
|
||||
return getCourSequel(media)
|
||||
}
|
||||
|
||||
const courRegex = /[2-9](?:nd|rd|th) Cour|Cour [2-9]|Part [2-9]/i
|
||||
|
||||
function isTitleSplitCour (media) {
|
||||
const titles = [...Object.values(media.title), ...media.synonyms]
|
||||
|
||||
console.log('checking cour titles', titles)
|
||||
|
||||
return titles.some(title => courRegex.test(title))
|
||||
}
|
||||
|
||||
const seasons = ['WINTER', 'SPRING', 'SUMMER', 'FALL']
|
||||
const getDate = ({ seasonYear, season }) => new Date(`${seasonYear}-${seasons.indexOf(season) * 4 || 1}-01`)
|
||||
|
||||
function getMediaDate (media) {
|
||||
if (media.startDate) return new Date(Object.values(media.startDate).join(' '))
|
||||
return getDate(media)
|
||||
}
|
||||
|
||||
function getCourSequel (media) {
|
||||
const mediaDate = getMediaDate(media)
|
||||
const animeRelations = media.relations.edges.filter(({ node, relationType }) => {
|
||||
if (node.type !== 'ANIME') return false
|
||||
if (node.status !== 'FINISHED') return false
|
||||
if (relationType !== 'SEQUEL') return false
|
||||
if (!['OVA', 'TV'].some(format => node.format === format)) return false // not movies or ona's
|
||||
if (mediaDate > getMediaDate(node)) return false // node needs to be released after media to be a sequel
|
||||
return isTitleSplitCour(node)
|
||||
})
|
||||
|
||||
if (!animeRelations.length) return false
|
||||
|
||||
// get closest sequel
|
||||
return animeRelations.reduce((prev, curr) => {
|
||||
return getMediaDate(prev) - mediaDate > getMediaDate(curr) - mediaDate ? curr : prev
|
||||
})
|
||||
}
|
||||
|
||||
function getCourPrequel (media) {
|
||||
const mediaDate = getMediaDate(media)
|
||||
const animeRelations = media.relations.edges.filter(({ node, relationType }) => {
|
||||
if (node.type !== 'ANIME') return false
|
||||
if (node.status !== 'FINISHED') return false
|
||||
if (relationType !== 'PREQUEL') return false
|
||||
if (!['OVA', 'TV'].some(format => node.format === format)) return false
|
||||
if (mediaDate < getMediaDate(node)) return false // node needs to be released before media to be a prequel
|
||||
return true
|
||||
}).map(({ node }) => node)
|
||||
|
||||
if (!animeRelations.length) {
|
||||
console.error('Detected split count but couldn\'t find prequel', media)
|
||||
return false
|
||||
}
|
||||
|
||||
// get closest prequel
|
||||
return animeRelations.reduce((prev, curr) => {
|
||||
return mediaDate - getMediaDate(prev) > mediaDate - getMediaDate(curr) ? curr : prev
|
||||
})
|
||||
}
|
||||
|
||||
function isMovie (media) {
|
||||
if (media.format === 'MOVIE') return true
|
||||
if ([...Object.values(media.title), ...media.synonyms].some(title => title.toLowerCase().includes('movie'))) return true
|
||||
if (!getParentForSpecial(media)) return true
|
||||
// if (!getParentForSpecial(media)) return true // TODO: this is good for checking movies, but false positives with normal TV shows
|
||||
return media.duration > 80 && media.episodes === 1
|
||||
}
|
||||
|
||||
|
|
@ -95,20 +185,23 @@ function buildQuery (quality) {
|
|||
return query
|
||||
}
|
||||
|
||||
async function fetchBatches ({ media, id, quality }) {
|
||||
// TODO: improve split-cour show batch lookup [tosho assigns aid's incorrectly]
|
||||
async function fetchBatches ({ episodeCount, id, quality }) {
|
||||
const queryString = buildQuery(quality)
|
||||
const torrents = await fetch(toshoURL + 'order=size-d&aid=' + id + queryString)
|
||||
|
||||
// safe if AL includes EP 0 or doesn't
|
||||
return (await torrents.json()).filter(entry => entry.num_files >= media.episodes)
|
||||
const batches = (await torrents.json()).filter(entry => entry.num_files >= episodeCount)
|
||||
console.log({ batches })
|
||||
return batches
|
||||
}
|
||||
|
||||
async function fetchSingleEpisode ({ id, quality }) {
|
||||
const queryString = buildQuery(quality)
|
||||
const torrents = await fetch(toshoURL + 'eid=' + id + queryString)
|
||||
|
||||
return torrents.json()
|
||||
const episodes = await torrents.json()
|
||||
console.log({ episodes })
|
||||
return episodes
|
||||
}
|
||||
|
||||
function mapTosho2dDeDupedEntry (entries) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default class Sections {
|
|||
|
||||
static createFallbackLoad (variables, type) {
|
||||
return (page = 1, perPage = 50, search = variables) => {
|
||||
const options = { method: 'Search', page, perPage, ...search }
|
||||
const options = { method: 'Search', page, perPage, ...Sections.sanitiseObject(search) }
|
||||
const res = alRequest(options)
|
||||
return Sections.wrapResponse(res, perPage, type)
|
||||
}
|
||||
|
|
@ -27,6 +27,14 @@ export default class Sections {
|
|||
return Array.from({ length }, (_, i) => ({ type, data: Sections.fromPending(res, i) }))
|
||||
}
|
||||
|
||||
static sanitiseObject (object = {}) {
|
||||
const safe = {}
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
if (value) safe[key] = value
|
||||
}
|
||||
return safe
|
||||
}
|
||||
|
||||
static async fromPending (arr, i) {
|
||||
const { data } = await arr
|
||||
return data.Page.media[i]
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
])
|
||||
}
|
||||
if (alToken) {
|
||||
manager.add([
|
||||
const sections = [
|
||||
{
|
||||
title: 'Continue Watching',
|
||||
load: (page = 1, perPage = 50, variables = {}) => {
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
if (media.status === 'FINISHED') return true
|
||||
return media.mediaListEntry?.progress < media.nextAiringEpisode?.episode - 1
|
||||
}).map(({ media }) => media.id)
|
||||
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...variables })
|
||||
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...Sections.sanitiseObject(variables) })
|
||||
})
|
||||
return Sections.wrapResponse(res, perPage)
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
return edge.relationType === 'SEQUEL'
|
||||
})
|
||||
}).map(({ node }) => node.id)
|
||||
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...variables, status: ['FINISHED', 'RELEASING'], onList: false })
|
||||
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...Sections.sanitiseObject(variables), status: ['FINISHED', 'RELEASING'], onList: false })
|
||||
})
|
||||
return Sections.wrapResponse(res, perPage)
|
||||
}
|
||||
|
|
@ -59,12 +59,19 @@
|
|||
load: (page = 1, perPage = 50, variables = {}) => {
|
||||
const res = userLists.value.then(res => {
|
||||
const ids = res.data.MediaListCollection.lists.find(({ status }) => status === 'PLANNING').entries.map(({ media }) => media.id)
|
||||
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...variables })
|
||||
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...Sections.sanitiseObject(variables) })
|
||||
})
|
||||
return Sections.wrapResponse(res, perPage)
|
||||
}
|
||||
}
|
||||
])
|
||||
]
|
||||
userLists.subscribe(() => {
|
||||
const titles = sections.map(({ title }) => title)
|
||||
for (const section of manager.sections) {
|
||||
if (titles.includes(section.title)) delete section.preview
|
||||
}
|
||||
})
|
||||
manager.add(sections)
|
||||
}
|
||||
manager.add([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,378 +0,0 @@
|
|||
<script context='module'>
|
||||
import { readable, writable } from 'simple-store-svelte'
|
||||
import { add } from '@/modules/torrent.js'
|
||||
import { alToken, set } from '../Settings.svelte'
|
||||
import { alRequest } from '@/modules/anilist.js'
|
||||
import { sleep } from '@/modules/util.js'
|
||||
import { resolveFileMedia } from '@/modules/anime.js'
|
||||
import { getRSSContent, getReleasesRSSurl } from '@/modules/rss.js'
|
||||
const noop = () => {}
|
||||
async function getVal (set) {
|
||||
const res = await fetch('https://gh.miru.workers.dev/')
|
||||
const amt = await res.text() / 100
|
||||
set(amt.toFixed(2))
|
||||
}
|
||||
const progress = readable(0, set => {
|
||||
getVal(set)
|
||||
return noop
|
||||
})
|
||||
const getSeason = d => seasons[Math.floor((d.getMonth() / 12) * 4) % 4]
|
||||
|
||||
let hasNext = true
|
||||
function processMedia (res, length) {
|
||||
res.then(res => {
|
||||
hasNext = res?.data?.Page.pageInfo.hasNextPage
|
||||
})
|
||||
|
||||
return Array.from({ length }, (_, i) => ({ type: 'full', data: fromPending(res, i) }))
|
||||
}
|
||||
async function fromPending (arr, i) {
|
||||
// return new Promise(r => r)
|
||||
const { data } = await arr
|
||||
return data.Page.media[i]
|
||||
}
|
||||
|
||||
const search = writable({})
|
||||
let lastRSSDate = 0
|
||||
async function releasesCards (page, limit, force, val) {
|
||||
const doc = await getRSSContent(getReleasesRSSurl(val))
|
||||
if (doc) {
|
||||
const pubDate = doc.querySelector('pubDate').textContent
|
||||
if (force || lastRSSDate !== pubDate) {
|
||||
lastRSSDate = pubDate
|
||||
const index = (page - 1) * limit
|
||||
const items = [...doc.querySelectorAll('item')].slice(index, index + limit)
|
||||
hasNext = items.length === limit
|
||||
const media = await resolveFileMedia(items.map(item => item.querySelector('title').textContent))
|
||||
media.forEach((mediaInformation, index) => {
|
||||
mediaInformation.date = new Date(items[index].querySelector('pubDate').textContent)
|
||||
mediaInformation.onclick = () => {
|
||||
add(items[index].querySelector('link').textContent)
|
||||
}
|
||||
})
|
||||
media.hasNext = hasNext
|
||||
return media
|
||||
}
|
||||
}
|
||||
}
|
||||
const seasons = ['WINTER', 'SPRING', 'SUMMER', 'FALL']
|
||||
function sanitiseObject (object) {
|
||||
const safe = {}
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
if (value) safe[key] = value
|
||||
}
|
||||
return safe
|
||||
}
|
||||
function customFilter (mediaList) {
|
||||
return mediaList?.filter(({ media }) => {
|
||||
let condition = true
|
||||
const { value } = search
|
||||
if (!media) return condition
|
||||
if (value.genre && !media.genres?.includes(value.genre)) condition = false
|
||||
if (value.season && media.season !== value.season) condition = false
|
||||
if (value.year && media.seasonYear !== value.year) condition = false
|
||||
if (value.format && media.format !== value.format) condition = false
|
||||
if (value.status && media.status !== value.status) condition = false
|
||||
if (value.search) {
|
||||
const titles = Object.values(media.title)
|
||||
.concat(media.synonyms)
|
||||
.filter(name => name != null)
|
||||
.map(title => title.toLowerCase())
|
||||
if (!titles.find(title => title.includes(value.search.toLowerCase()))) condition = false
|
||||
}
|
||||
return condition
|
||||
})
|
||||
}
|
||||
|
||||
let lastDate = null
|
||||
let sections = {
|
||||
// continue: {
|
||||
// title: 'Continue Watching',
|
||||
// preview: () => sections.continue.load(1, 6),
|
||||
// load: (page = 1, perPage = 50, initial = false) => {
|
||||
// if (initial) search.value = { ...search.value, sort: 'UPDATED_TIME_DESC' }
|
||||
// return alRequest({ method: 'UserLists', status_in: ['CURRENT', 'REPEATING'], page }).then(res => {
|
||||
// hasNext = res?.data?.Page.pageInfo.hasNextPage
|
||||
// return customFilter(
|
||||
// res?.data?.Page.mediaList
|
||||
// .filter(i => {
|
||||
// return i.media.status !== 'RELEASING' || i.media.mediaListEntry?.progress < i.media.nextAiringEpisode?.episode - 1
|
||||
// })
|
||||
// .slice(0, perPage)
|
||||
// )
|
||||
// })
|
||||
// },
|
||||
// hide: !alToken
|
||||
// },
|
||||
// newSeasons: {
|
||||
// title: 'Sequels You Missed',
|
||||
// data: (async () => {
|
||||
// if (!alToken) return
|
||||
// const { data } = await alRequest({ method: 'NewSeasons' })
|
||||
// const res = data.MediaListCollection.lists[0]
|
||||
// return res?.entries?.flatMap(({ media }) => {
|
||||
// return media.relations.edges.filter(edge => {
|
||||
// return edge.relationType === 'SEQUEL' && !edge.node.mediaListEntry
|
||||
// })
|
||||
// }).map(({ node }) => node.id)
|
||||
// })(),
|
||||
// preview: () => sections.newSeasons.load(1, 6),
|
||||
// load: async (page = 1, perPage = 50, initial = false) => {
|
||||
// if (initial) search.value = { ...search.value, status: 'FINISHED' }
|
||||
// const id = await sections.newSeasons.data
|
||||
// const res = await alRequest({ method: 'SearchIDS', page, perPage, id, ...sanitiseObject(search.value), status: ['FINISHED', 'RELEASING'], onList: false })
|
||||
// return processMedia(res)
|
||||
// },
|
||||
// hide: !alToken
|
||||
// },
|
||||
// planning: {
|
||||
// title: 'Your List',
|
||||
// preview: () => sections.planning.load(1, 6),
|
||||
// load: (page = 1, perPage = 50, initial = false) => {
|
||||
// if (initial) search.value = { ...search.value, sort: 'UPDATED_TIME_DESC' }
|
||||
// return alRequest({ method: 'UserLists', page, perPage, status_in: 'PLANNING' }).then(res => {
|
||||
// hasNext = res?.data?.Page.pageInfo.hasNextPage
|
||||
// return customFilter(res?.data?.Page.mediaList)
|
||||
// })
|
||||
// },
|
||||
// hide: !alToken
|
||||
// },
|
||||
seasonal: {
|
||||
title: 'Popular This Season',
|
||||
preview: function () {
|
||||
if (!this.previewData) this.previewData = this.load(1, 10)
|
||||
return this.previewData
|
||||
},
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
const date = new Date()
|
||||
if (initial) {
|
||||
search.value = {
|
||||
...search.value,
|
||||
season: getSeason(date),
|
||||
year: date.getFullYear(),
|
||||
sort: 'POPULARITY_DESC'
|
||||
}
|
||||
}
|
||||
return processMedia(alRequest({ method: 'Search', page, perPage, year: date.getFullYear(), season: getSeason(date), sort: 'POPULARITY_DESC', ...sanitiseObject(search.value) }), perPage)
|
||||
}
|
||||
},
|
||||
trending: {
|
||||
title: 'Trending Now',
|
||||
preview: function () {
|
||||
if (!this.previewData) this.previewData = this.load(1, 10)
|
||||
return this.previewData
|
||||
},
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) search.value = { ...search.value, sort: 'TRENDING_DESC' }
|
||||
return processMedia(alRequest({ method: 'Search', page, perPage, sort: 'TRENDING_DESC', ...sanitiseObject(search.value) }), perPage)
|
||||
}
|
||||
},
|
||||
popular: {
|
||||
title: 'All Time Popular',
|
||||
preview: function () {
|
||||
if (!this.previewData) this.previewData = this.load(1, 10)
|
||||
return this.previewData
|
||||
},
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) search.value = { ...search.value, sort: 'POPULARITY_DESC' }
|
||||
return processMedia(alRequest({ method: 'Search', page, perPage, sort: 'POPULARITY_DESC', ...sanitiseObject(search.value) }), perPage)
|
||||
}
|
||||
},
|
||||
romance: {
|
||||
title: 'Romance',
|
||||
preview: () => {
|
||||
const self = sections.romance
|
||||
if (!self.previewData) self.previewData = self.load(1, 10)
|
||||
return self.previewData
|
||||
},
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) search.value = { ...search.value, sort: 'TRENDING_DESC', genre: 'Romance' }
|
||||
return processMedia(alRequest({ method: 'Search', page, perPage, sort: 'TRENDING_DESC', genre: 'Romance', ...sanitiseObject(search.value) }), perPage)
|
||||
}
|
||||
},
|
||||
action: {
|
||||
title: 'Action',
|
||||
preview: () => {
|
||||
const self = sections.action
|
||||
if (!self.previewData) self.previewData = self.load(1, 10)
|
||||
return self.previewData
|
||||
},
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) search.value = { ...search.value, sort: 'TRENDING_DESC', genre: 'Action' }
|
||||
return processMedia(alRequest({ method: 'Search', page, perPage, sort: 'TRENDING_DESC', genre: 'Action', ...sanitiseObject(search.value) }), perPage)
|
||||
}
|
||||
},
|
||||
adventure: {
|
||||
title: 'Adventure',
|
||||
preview: () => {
|
||||
const self = sections.adventure
|
||||
if (!self.previewData) self.previewData = self.load(1, 10)
|
||||
return self.previewData
|
||||
},
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) search.value = { ...search.value, sort: 'TRENDING_DESC', genre: 'Adventure' }
|
||||
return processMedia(alRequest({ method: 'Search', page, perPage, sort: 'TRENDING_DESC', genre: 'Adventure', ...sanitiseObject(search.value) }), perPage)
|
||||
}
|
||||
},
|
||||
fantasy: {
|
||||
title: 'Fantasy',
|
||||
preview: () => {
|
||||
const self = sections.fantasy
|
||||
if (!self.previewData) self.previewData = self.load(1, 10)
|
||||
return self.previewData
|
||||
},
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) search.value = { ...search.value, sort: 'TRENDING_DESC', genre: 'Fantasy' }
|
||||
return processMedia(alRequest({ method: 'Search', page, perPage, sort: 'TRENDING_DESC', genre: 'Fantasy', ...sanitiseObject(search.value) }), perPage)
|
||||
}
|
||||
},
|
||||
comedy: {
|
||||
title: 'Comedy',
|
||||
preview: () => {
|
||||
const self = sections.comedy
|
||||
if (!self.previewData) self.previewData = self.load(1, 10)
|
||||
return self.previewData
|
||||
},
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
if (initial) search.value = { ...search.value, sort: 'TRENDING_DESC', genre: 'Comedy' }
|
||||
return processMedia(alRequest({ method: 'Search', page, perPage, sort: 'TRENDING_DESC', genre: 'Comedy', ...sanitiseObject(search.value) }), perPage)
|
||||
}
|
||||
},
|
||||
schedule: {
|
||||
title: 'Schedule',
|
||||
hide: true,
|
||||
load: (page = 1, perPage = 50, initial = false) => {
|
||||
const date = new Date()
|
||||
if (initial) search.value = { ...search.value, sort: 'START_DATE_DESC', status: 'RELEASING' }
|
||||
if (perPage !== 6) date.setHours(0, 0, 0, 0)
|
||||
return alRequest({ method: 'AiringSchedule', page, from: parseInt(date.getTime() / 1000) }).then(res => {
|
||||
const entries = customFilter(res?.data?.Page.airingSchedules.filter(entry => entry.media.countryOfOrigin !== 'CN' && !entry.media.isAdult) || []).slice(0, perPage)
|
||||
const media = []
|
||||
hasNext = res?.data?.Page.pageInfo.hasNextPage
|
||||
const date = new Date()
|
||||
for (const entry of entries) {
|
||||
if (entry.timeUntilAiring && perPage !== 6 && (!lastDate || new Date(+date + entry.timeUntilAiring * 1000).getDay() !== lastDate.getDay())) {
|
||||
lastDate = new Date(+date + entry.timeUntilAiring * 1000)
|
||||
media.push(lastDate.toLocaleDateString('en-US', { weekday: 'long' }))
|
||||
}
|
||||
entry.schedule = true
|
||||
media.push(entry)
|
||||
}
|
||||
return media
|
||||
})
|
||||
}
|
||||
},
|
||||
search: {
|
||||
title: 'Search',
|
||||
hide: true,
|
||||
load: (page = 1, perPage = 50) => {
|
||||
const opts = {
|
||||
method: 'Search',
|
||||
page,
|
||||
perPage,
|
||||
...sanitiseObject(search.value)
|
||||
}
|
||||
return processMedia(alRequest(opts), perPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = set.rssFeeds.length - 1; i >= 0; --i) {
|
||||
const [title, val] = set.rssFeeds[i]
|
||||
const section = {
|
||||
title,
|
||||
load: async (page = 1, perPage = 20, initial = false, force = true) => {
|
||||
if (initial) search.value = { ...search.value, sort: 'START_DATE_DESC' }
|
||||
return customFilter(await releasesCards(page, Math.min(perPage, 13), force, val))
|
||||
},
|
||||
preview: async () => {
|
||||
const self = sections['releases-' + i]
|
||||
if (!self.previewData) {
|
||||
await sleep(i * 3000) // stagger lists by 3 seconds
|
||||
setInterval(async () => {
|
||||
const newData = await self.load(1, 6, false, false)
|
||||
if (newData) self.previewData = newData
|
||||
}, 15000)
|
||||
self.previewData = await self.load(1, 6, false, true)
|
||||
}
|
||||
return self.previewData
|
||||
}
|
||||
}
|
||||
sections = {
|
||||
['releases-' + i]: section,
|
||||
...sections
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Search from './Search.svelte'
|
||||
import Section from './Section.svelte'
|
||||
import Gallery from './Gallery.svelte'
|
||||
|
||||
let media = []
|
||||
export let current = null
|
||||
let page = 1
|
||||
|
||||
let canScroll = true
|
||||
let container = null
|
||||
|
||||
function infiniteScroll () {
|
||||
if (current && canScroll && hasNext && this.scrollTop + this.clientHeight > this.scrollHeight - 800) {
|
||||
infiniteScrollLoad()
|
||||
}
|
||||
}
|
||||
async function infiniteScrollLoad () {
|
||||
canScroll = false
|
||||
const res = sections[current].load(++page)
|
||||
media.push(res)
|
||||
media = media
|
||||
await res
|
||||
canScroll = hasNext
|
||||
}
|
||||
|
||||
async function loadCurrent (initial = true) {
|
||||
page = 1
|
||||
canScroll = false
|
||||
const res = sections[current].load(1, 50, initial)
|
||||
media = [res]
|
||||
await res
|
||||
canScroll = hasNext
|
||||
if ((await res).length < 12 && hasNext) infiniteScrollLoad()
|
||||
}
|
||||
|
||||
$: load(current)
|
||||
async function load (current) {
|
||||
if (sections[current]) {
|
||||
loadCurrent()
|
||||
} else {
|
||||
if (container) container.scrollTop = 0
|
||||
media = []
|
||||
canScroll = true
|
||||
lastDate = null
|
||||
$search = {
|
||||
search: null,
|
||||
genre: '',
|
||||
season: '',
|
||||
year: null,
|
||||
format: '',
|
||||
status: '',
|
||||
sort: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class='h-full w-full overflow-y-scroll root overflow-x-hidden' on:scroll={infiniteScroll} bind:this={container}>
|
||||
<div class='d-flex flex-column h-full w-full'>
|
||||
<Search bind:media bind:search={$search} bind:current {loadCurrent} />
|
||||
{#if media.length}
|
||||
<Gallery {media} />
|
||||
{:else}
|
||||
{#each Object.entries(sections) as [key, opts] (key)}
|
||||
{#if !opts.hide}
|
||||
<Section opts={{ ...opts, onclick: () => (current = key) }} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -13,17 +13,30 @@
|
|||
import smoothScroll from '@/modules/scroll.js'
|
||||
import { debounce } from '@/modules/util.js'
|
||||
|
||||
let page = 1
|
||||
|
||||
function loadSearchData (search) {
|
||||
const load = search.load || Sections.createFallbackLoad()
|
||||
$items = load(1, undefined, searchCleanup(search))
|
||||
$items = load(page, undefined, searchCleanup(search))
|
||||
}
|
||||
loadSearchData($search)
|
||||
const update = debounce(loadSearchData, 150)
|
||||
|
||||
// TODO: infinite scrolling
|
||||
let canScroll = true
|
||||
const hasNextPage = true
|
||||
|
||||
async function infiniteScroll () {
|
||||
if (canScroll && hasNextPage && this.scrollTop + this.clientHeight > this.scrollHeight - 800) {
|
||||
canScroll = false
|
||||
const load = search.load || Sections.createFallbackLoad()
|
||||
const nextData = load(++page, undefined, searchCleanup(search))
|
||||
$items = [...$items, ...nextData]
|
||||
nextData[nextData.length - 1].data.then(() => { canScroll = true })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class='h-full w-full overflow-y-scroll d-flex flex-wrap flex-row root overflow-x-hidden px-50 justify-content-center align-content-start' use:smoothScroll>
|
||||
<div class='h-full w-full overflow-y-scroll d-flex flex-wrap flex-row root overflow-x-hidden px-50 justify-content-center align-content-start' use:smoothScroll on:scroll={infiniteScroll}>
|
||||
<Search bind:search={$search} on:input={() => update($search)} />
|
||||
{#each $items as card}
|
||||
<Card {card} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue