fix: better userList caching and sorting

This commit is contained in:
RockinChaos 2024-05-29 17:06:37 -07:00
parent 2862b0235c
commit f22b497e03
5 changed files with 115 additions and 89 deletions

View file

@ -144,7 +144,7 @@ export type MediaListCollection = {
lists: {
status: string
entries: {
media: MediaListMedia
media: Media
}[]
}[]
}

View file

@ -203,9 +203,9 @@ class AnilistClient {
})
if (this.userID?.viewer?.data?.Viewer) {
this.userLists.value = this.getUserLists()
this.userLists.value = this.getUserLists({ sort: 'UPDATED_TIME_DESC' })
// update userLists every 15 mins
setInterval(() => { this.userLists.value = this.getUserLists() }, 1000 * 60 * 15)
setInterval(() => this.userLists.value = this.getUserLists({ sort: 'UPDATED_TIME_DESC' }), 1000 * 60 * 15)
}
}
@ -257,8 +257,8 @@ class AnilistClient {
variables: {
sort: 'TRENDING_DESC',
page: 1,
perPage: 30,
status_in: '[CURRENT,PLANNING]',
perPage: 50,
status_in: '[CURRENT,PLANNING,COMPLETED,DROPPED,PAUSED,REPEATING]',
...variables
}
})
@ -369,7 +369,7 @@ class AnilistClient {
variables.lists.push('Watched using Miru')
}
await this.entry(variables)
this.userLists.value = this.getUserLists()
this.userLists.value = this.getUserLists({ sort: 'UPDATED_TIME_DESC' })
}
}
}
@ -458,40 +458,29 @@ class AnilistClient {
}
/** @returns {Promise<import('./al.d.ts').Query<{ MediaListCollection: import('./al.d.ts').MediaListCollection }>>} */
async getUserLists (variables = {}) {
async getUserLists (variables) {
const userId = this.userID?.viewer?.data?.Viewer.id
variables.id = userId
if (variables.sort) { variables.sort = variables.sort.replace('USER_SCORE_DESC', 'SCORE_DESC')}
const query = /* js */`
query($id: Int){
MediaListCollection(userId: $id, type: ANIME, forceSingleCompletedList: true) {
query($id: Int $sort: [MediaListSort]){
MediaListCollection(userId: $id, type: ANIME, sort: $sort, forceSingleCompletedList: true) {
lists {
status,
entries {
media {
id,
status,
mediaListEntry {
progress
},
nextAiringEpisode {
episode
},
relations {
edges {
relationType(version:2)
node {
id
}
}
}
${queryObjects}
}
}
}
}
}`
// this doesn't need to be cached, as SearchIDStatus is already cached, which is the only thing that uses this
return await this.alRequest(query, variables)
const res = await this.alRequest(query, variables)
this.updateCache(res.data.MediaListCollection.lists.flatMap(list => list.entries.map(entry => entry.media)));
return res
}
/** @returns {Promise<import('./al.d.ts').Query<{ MediaList: { status: string, progress: number, repeat: number }}>>} */

View file

@ -24,8 +24,7 @@ export default class SectionsManager {
static createFallbackLoad (variables, type) {
return (page = 1, perPage = 50, search = variables) => {
const options = { page, perPage, ...SectionsManager.sanitiseObject(search) }
const res = anilistClient.search(options)
const res = anilistClient.search({ page, perPage, ...SectionsManager.sanitiseObject(search) })
return SectionsManager.wrapResponse(res, perPage, type)
}
}
@ -49,6 +48,44 @@ export default class SectionsManager {
const { data } = await arr
return data?.Page.media[i]
}
static isUserSort(variables) {
return ['UPDATED_TIME_DESC', 'STARTED_ON_DESC', 'FINISHED_ON_DESC', 'PROGRESS_DESC', 'USER_SCORE_DESC'].includes(variables.sort);
}
static async getPaginatedMediaList(_page, perPage, variables, mediaList) {
const ids = mediaList.filter(({ media }) => {
if ((!variables.search || (media.title.userPreferred && media.title.userPreferred.toLowerCase().includes(variables.search.toLowerCase())) || (media.title.english && media.title.english.toLowerCase().includes(variables.search.toLowerCase())) || (media.title.romaji && media.title.romaji.toLowerCase().includes(variables.search.toLowerCase())) || (media.title.native && media.title.native.toLowerCase().includes(variables.search.toLowerCase()))) &&
(!variables.genre || variables.genre.map(genre => genre.trim().toLowerCase()).every(genre => media.genres.map(genre => genre.trim().toLowerCase()).includes(genre))) &&
(!variables.tag || variables.tag.map(tag => tag.trim().toLowerCase()).every(tag => media.tags.map(tag => tag.name.trim().toLowerCase()).includes(tag))) &&
(!variables.season || variables.season === media.season) &&
(!variables.year || variables.year === media.seasonYear) &&
(!variables.format || variables.format === media.format) &&
(!variables.status || variables.status === media.status) &&
(!variables.continueWatching || (media.status === 'FINISHED' || media.mediaListEntry?.progress < media.nextAiringEpisode?.episode - 1))) {
return true;
}
}).map(({ media }) => (this.isUserSort(variables) ? media : media.id));
if (!ids.length) return {}
if (this.isUserSort(variables)) {
const startIndex = (perPage * (_page - 1));
const endIndex = startIndex + perPage;
const paginatedIds = ids.slice(startIndex, endIndex);
const hasNextPage = ids.length > endIndex;
return {
data: {
Page: {
pageInfo: {
hasNextPage: hasNextPage
},
media: paginatedIds
}
}
}
} else {
return anilistClient.searchIDS({ _page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
}
}
}
// list of all possible home screen sections
@ -80,24 +117,6 @@ function createSections () {
return section
}),
// user specific sections
{
title: 'Continue Watching',
load: (page = 1, perPage = 50, variables = {}) => {
const res = anilistClient.userLists.value.then(res => {
const mediaList = res.data.MediaListCollection.lists.reduce((filtered, { status, entries }) => {
return (status === 'CURRENT' || status === 'REPEATING') ? filtered.concat(entries) : filtered
}, [])
const ids = mediaList.filter(({ media }) => {
if (media.status === 'FINISHED') return true
return media.mediaListEntry?.progress < media.nextAiringEpisode?.episode - 1
}).map(({ media }) => media.id)
if (!ids.length) return {}
return anilistClient.searchIDS({ page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
{
title: 'Sequels You Missed',
load: (page = 1, perPage = 50, variables = {}) => {
@ -115,73 +134,92 @@ function createSections () {
hide: !alToken
},
{
title: 'Your List',
load: (page = 1, perPage = 50, variables = {}) => {
const res = anilistClient.userLists.value.then(res => {
const ids = res.data.MediaListCollection.lists.find(({ status }) => status === 'PLANNING')?.entries.map(({ media }) => media.id)
if (!ids) return {}
return anilistClient.searchIDS({ page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
title: 'Continue Watching', variables: { sort: 'UPDATED_TIME_DESC', userList: true, continueWatching: true },
load: (_page = 1, perPage = 50, variables = {}) => {
const userLists = (!SectionsManager.isUserSort(variables) || variables.sort === 'UPDATED_TIME_DESC') ? anilistClient.userLists.value : anilistClient.getUserLists({ sort: variables.sort })
const res = userLists.then(res => {
const mediaList = res.data.MediaListCollection.lists.reduce((filtered, { status, entries }) => {
return (status === 'CURRENT' || status === 'REPEATING') ? filtered.concat(entries) : filtered
}, []);
if (!mediaList) return {}
return SectionsManager.getPaginatedMediaList(_page, perPage, variables, mediaList)
});
return SectionsManager.wrapResponse(res, perPage);
},
hide: !alToken
},
{
title: 'Watching List', variables : { sort: 'UPDATED_TIME_DESC', userList: true },
load: (_page = 1, perPage = 50, variables = {}) => {
const userLists = (!SectionsManager.isUserSort(variables) || variables.sort === 'UPDATED_TIME_DESC') ? anilistClient.userLists.value : anilistClient.getUserLists({ sort: variables.sort })
const res = userLists.then(res => {
const mediaList = res.data.MediaListCollection.lists.find(({ status }) => status === 'CURRENT')?.entries
if (!mediaList) return {}
return SectionsManager.getPaginatedMediaList(_page, perPage, variables, mediaList)
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
{
title: 'Completed List',
load: (page = 1, perPage = 50, variables = {}) => {
const res = anilistClient.userLists.value.then(res => {
const ids = res.data.MediaListCollection.lists.find(({ status }) => status === 'COMPLETED')?.entries.map(({ media }) => media.id)
if (!ids) return {}
return anilistClient.searchIDS({ page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
title: 'Completed List', variables : { sort: 'UPDATED_TIME_DESC', userList: true, completedList: true },
load: (_page = 1, perPage = 50, variables = {}) => {
const userLists = (!SectionsManager.isUserSort(variables) || variables.sort === 'UPDATED_TIME_DESC') ? anilistClient.userLists.value : anilistClient.getUserLists({ sort: variables.sort })
const res = userLists.then(res => {
const mediaList = res.data.MediaListCollection.lists.find(({ status }) => status === 'COMPLETED')?.entries
if (!mediaList) return {}
return SectionsManager.getPaginatedMediaList(_page, perPage, variables, mediaList)
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
{
title: 'Paused List',
load: (page = 1, perPage = 50, variables = {}) => {
title: 'Planning List', variables : { sort: 'POPULARITY_DESC', userList: true },
load: (_page = 1, perPage = 50, variables = {}) => {
const res = anilistClient.userLists.value.then(res => {
const ids = res.data.MediaListCollection.lists.find(({ status }) => status === 'PAUSED')?.entries.map(({ media }) => media.id)
if (!ids) return {}
return anilistClient.searchIDS({ page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
const mediaList = res.data.MediaListCollection.lists.find(({ status }) => status === 'PLANNING')?.entries
if (!mediaList) return {}
return SectionsManager.getPaginatedMediaList(_page, perPage, variables, mediaList)
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
{
title: 'Dropped List',
load: (page = 1, perPage = 50, variables = {}) => {
const res = anilistClient.userLists.value.then(res => {
const ids = res.data.MediaListCollection.lists.find(({ status }) => status === 'DROPPED')?.entries.map(({ media }) => media.id)
if (!ids) return {}
return anilistClient.searchIDS({ page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
title: 'Paused List', variables : { sort: 'UPDATED_TIME_DESC', userList: true },
load: (_page = 1, perPage = 50, variables = {}) => {
const userLists = (!SectionsManager.isUserSort(variables) || variables.sort === 'UPDATED_TIME_DESC') ? anilistClient.userLists.value : anilistClient.getUserLists({ sort: variables.sort })
const res = userLists.then(res => {
const mediaList = res.data.MediaListCollection.lists.find(({ status }) => status === 'PAUSED')?.entries
if (!mediaList) return {}
return SectionsManager.getPaginatedMediaList(_page, perPage, variables, mediaList)
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
{
title: 'Currently Watching List',
load: (page = 1, perPage = 50, variables = {}) => {
const res = anilistClient.userLists.value.then(res => {
const ids = res.data.MediaListCollection.lists.find(({ status }) => status === 'CURRENT')?.entries.map(({ media }) => media.id)
if (!ids) return {}
return anilistClient.searchIDS({ page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
title: 'Dropped List', variables : { sort: 'UPDATED_TIME_DESC', userList: true},
load: (_page = 1, perPage = 50, variables = {}) => {
const userLists = (!SectionsManager.isUserSort(variables) || variables.sort === 'UPDATED_TIME_DESC') ? anilistClient.userLists.value : anilistClient.getUserLists({ sort: variables.sort })
const res = userLists.then(res => {
const mediaList = res.data.MediaListCollection.lists.find(({ status }) => status === 'DROPPED')?.entries
if (!mediaList) return {}
return SectionsManager.getPaginatedMediaList(_page, perPage, variables, mediaList)
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
// common, non-user specific sections
{ title: 'Popular This Season', variables: { sort: 'POPULARITY_DESC', season: currentSeason, year: currentYear } },
{ title: 'Trending Now', variables: { sort: 'TRENDING_DESC' } },
{ title: 'All Time Popular', variables: { sort: 'POPULARITY_DESC' } },
{ title: 'Romance', variables: { sort: 'TRENDING_DESC', genre: 'Romance' } },
{ title: 'Action', variables: { sort: 'TRENDING_DESC', genre: 'Action' } },
{ title: 'Adventure', variables: { sort: 'TRENDING_DESC', genre: 'Adventure' } },
{ title: 'Fantasy', variables: { sort: 'TRENDING_DESC', genre: 'Fantasy' } },
{ title: 'Comedy', variables: { sort: 'TRENDING_DESC', genre: 'Comedy' } }
{ title: 'Popular This Season', variables: { sort: 'POPULARITY_DESC'} },
{ title: 'Trending Now', variables: { sort: 'TRENDING_DESC'} },
{ title: 'All Time Popular', variables: { sort: 'POPULARITY_DESC'} },
{ title: 'Romance', variables: { sort: 'TRENDING_DESC', genre: ['Romance']} },
{ title: 'Action', variables: { sort: 'TRENDING_DESC', genre: ['Action']} },
{ title: 'Adventure', variables: { sort: 'TRENDING_DESC', genre: ['Adventure'] } },
{ title: 'Fantasy', variables: { sort: 'TRENDING_DESC', genre: ['Fantasy']} },
{ title: 'Comedy', variables: { sort: 'TRENDING_DESC', genre: ['Comedy'] } }
]
}
}

View file

@ -15,7 +15,7 @@ try {
} catch (e) {}
try {
scopedDefaults = {
homeSections: [...(storedSettings.rssFeedsNew || defaults.rssFeedsNew).map(([title]) => title), 'Continue Watching', 'Sequels You Missed', 'Your List', 'Popular This Season', 'Trending Now', 'All Time Popular', 'Romance', 'Action', 'Adventure', 'Fantasy', 'Comedy']
homeSections: [...(storedSettings.rssFeedsNew || defaults.rssFeedsNew).map(([title]) => title), 'Continue Watching', 'Sequels You Missed', 'Planning List', 'Popular This Season', 'Trending Now', 'All Time Popular', 'Romance', 'Action', 'Adventure', 'Fantasy', 'Comedy']
}
} catch (e) {
resetSettings()

View file

@ -16,13 +16,12 @@
for (const sectionTitle of settings.value.homeSections) manager.add(mappedSections[sectionTitle])
if (anilistClient.userID?.viewer?.data?.Viewer) {
const userSections = ['Continue Watching', 'Sequels You Missed', 'Your List', 'Completed List', 'Paused List', 'Dropped List', 'Currently Watching List']
const userSections = ['Continue Watching', 'Sequels You Missed', 'Planning List', 'Completed List', 'Paused List', 'Dropped List', 'Watching List']
anilistClient.userLists.subscribe(value => {
if (!value) return
for (const section of manager.sections) {
// remove preview value, to force UI to re-request data, which updates it once in viewport
if (userSections.includes(section.title)) section.preview.value = section.load(1, 15)
if (userSections.includes(section.title)) section.preview.value = section.load(1, 15, section.variables)
}
})
}