diff --git a/src/sonolus/background/info.ts b/src/sonolus/background/info.ts index 8cffb2f..f91b317 100644 --- a/src/sonolus/background/info.ts +++ b/src/sonolus/background/info.ts @@ -1,11 +1,18 @@ import { Icon, Text } from '@sonolus/core' +import { BackgroundItemModel } from '@sonolus/express' import { randomize } from '../../utils/math.js' import { sonolus } from '../index.js' +import { hideSpoilers } from '../utils/spoiler.js' import { backgroundSearches } from './search.js' export const installBackgroundInfo = () => { - sonolus.background.infoHandler = () => { - const cardBackgrounds = sonolus.background.items.filter(({ meta }) => meta) + sonolus.background.infoHandler = ({ options }) => { + const cardBackgrounds = hideSpoilers( + options.spoilers, + sonolus.background.items.filter( + (item): item is BackgroundItemModel & { meta: object } => item.meta !== undefined, + ), + ) const otherBackgrounds = sonolus.background.items.filter(({ meta }) => !meta) return { diff --git a/src/sonolus/background/item.ts b/src/sonolus/background/item.ts index da5b831..eef8f1d 100644 --- a/src/sonolus/background/item.ts +++ b/src/sonolus/background/item.ts @@ -60,7 +60,7 @@ export const updateBackgroundItems = (repository: Repository) => { rarityIndex: cardRarity.index, attributeIndex: attribute.index, id: card.id, - releaseAt: card.releaseAt, + publishedAt: card.releaseAt, } if (cardRarity.hasTraining) { @@ -96,6 +96,6 @@ export const updateBackgroundItems = (repository: Repository) => { sonolus.background.items = [ ...initialBackgrounds, - ...backgrounds.sort((a, b) => b.meta.releaseAt - a.meta.releaseAt), + ...backgrounds.sort((a, b) => b.meta.publishedAt - a.meta.publishedAt), ] } diff --git a/src/sonolus/background/list.ts b/src/sonolus/background/list.ts index 44e459e..36e3304 100644 --- a/src/sonolus/background/list.ts +++ b/src/sonolus/background/list.ts @@ -1,16 +1,28 @@ -import { filterBackgrounds, paginateItems } from '@sonolus/express' +import { BackgroundItemModel, filterBackgrounds, paginateItems } from '@sonolus/express' import { sonolus } from '../index.js' import { randomizeItems, toIndexes } from '../utils/list.js' +import { hideSpoilers } from '../utils/spoiler.js' import { backgroundSearches } from './search.js' export const installBackgroundList = () => { - sonolus.background.listHandler = ({ search: { type, options }, page }) => { + sonolus.background.listHandler = ({ + search: { type, options }, + page, + options: serverOptions, + }) => { + const filteredBackgrounds = [ + ...hideSpoilers( + serverOptions.spoilers, + sonolus.background.items.filter( + (item): item is BackgroundItemModel & { meta: object } => + item.meta !== undefined, + ), + ), + ...sonolus.background.items.filter(({ meta }) => !meta), + ] if (type === 'quick') return { - ...paginateItems( - filterBackgrounds(sonolus.background.items, options.keywords), - page, - ), + ...paginateItems(filterBackgrounds(filteredBackgrounds, options.keywords), page), searches: backgroundSearches, } @@ -32,7 +44,7 @@ export const installBackgroundList = () => { const imageIndexes = toIndexes(options.images) const items = filterBackgrounds( - sonolus.background.items.filter( + filteredBackgrounds.filter( ({ meta }) => meta && characterIndexes.includes(meta.characterIndex) && diff --git a/src/sonolus/background/model.ts b/src/sonolus/background/model.ts index 3d1c14d..184c703 100644 --- a/src/sonolus/background/model.ts +++ b/src/sonolus/background/model.ts @@ -7,7 +7,7 @@ declare module '@sonolus/express' { attributeIndex: number imageIndex: number id: number - releaseAt: number + publishedAt: number } } } diff --git a/src/sonolus/configuration.ts b/src/sonolus/configuration.ts new file mode 100644 index 0000000..76ba002 --- /dev/null +++ b/src/sonolus/configuration.ts @@ -0,0 +1,15 @@ +import { ServerOptionsModel } from '@sonolus/express' + +export const serverOptions = { + spoilers: { + type: 'toggle', + name: { + en: 'Spoilers', + }, + def: false, + description: { + en: 'Whether to show unreleased items.', + }, + required: false, + }, +} as const satisfies ServerOptionsModel diff --git a/src/sonolus/index.ts b/src/sonolus/index.ts index 21471d8..be78881 100644 --- a/src/sonolus/index.ts +++ b/src/sonolus/index.ts @@ -3,6 +3,7 @@ import { config } from '../config.js' import { Repository } from '../repository/index.js' import { installBackground, updateBackground } from './background/index.js' import { backgroundSearches } from './background/search.js' +import { serverOptions } from './configuration.js' import { installEngine } from './engine/index.js' import { installInfo } from './info/index.js' import { installLevel, updateLevel } from './level/index.js' @@ -25,6 +26,9 @@ export const sonolus = new Sonolus({ background: { searches: backgroundSearches, }, + configuration: { + options: serverOptions, + }, }) installPack() diff --git a/src/sonolus/level/info.ts b/src/sonolus/level/info.ts index de2026c..7f1621b 100644 --- a/src/sonolus/level/info.ts +++ b/src/sonolus/level/info.ts @@ -2,16 +2,17 @@ import { Icon, Text } from '@sonolus/core' import { LevelItemModel } from '@sonolus/express' import { randomize } from '../../utils/math.js' import { sonolus } from '../index.js' +import { hideSpoilers } from '../utils/spoiler.js' import { levelSearches } from './search.js' export const installLevelInfo = () => { - sonolus.level.infoHandler = () => { + sonolus.level.infoHandler = ({ options }) => { const randomLevels: Record = {} const newestMusicIds = new Set() const newestLevels: LevelItemModel[] = [] - for (const level of sonolus.level.items) { + for (const level of hideSpoilers(options.spoilers, sonolus.level.items)) { randomLevels[`${level.meta.musicId}-${level.meta.musicVocalId}`] ??= level if (newestLevels.length >= 5) continue diff --git a/src/sonolus/level/list.ts b/src/sonolus/level/list.ts index 5f0e347..4f92184 100644 --- a/src/sonolus/level/list.ts +++ b/src/sonolus/level/list.ts @@ -1,13 +1,15 @@ import { filterLevels, paginateItems } from '@sonolus/express' import { sonolus } from '../index.js' import { randomizeItems, toIndexes } from '../utils/list.js' +import { hideSpoilers } from '../utils/spoiler.js' import { levelSearches } from './search.js' export const installLevelList = () => { - sonolus.level.listHandler = ({ search: { type, options }, page }) => { + sonolus.level.listHandler = ({ search: { type, options }, page, options: serverOptions }) => { + const filteredLevels = hideSpoilers(serverOptions.spoilers, sonolus.level.items) if (type === 'quick') return { - ...paginateItems(filterLevels(sonolus.level.items, options.keywords), page), + ...paginateItems(filterLevels(filteredLevels, options.keywords), page), searches: levelSearches, } @@ -16,7 +18,7 @@ export const installLevelList = () => { const difficultyIndexes = toIndexes(options.difficulties) const items = filterLevels( - sonolus.level.items.filter( + filteredLevels.filter( ({ rating, meta }) => (!meta.characterIndexes.length || meta.characterIndexes.some((characterIndex) => diff --git a/src/sonolus/playlist/details.ts b/src/sonolus/playlist/details.ts index 709b525..31bce4a 100644 --- a/src/sonolus/playlist/details.ts +++ b/src/sonolus/playlist/details.ts @@ -5,9 +5,10 @@ import { config } from '../../config.js' import { randomize } from '../../utils/math.js' import { sonolus } from '../index.js' import { nonEmpty } from '../utils/section.js' +import { hideSpoilers, hideSpoilersFromPlaylist } from '../utils/spoiler.js' export const installPlaylistDetails = () => { - sonolus.playlist.detailsHandler = ({ itemName }) => { + sonolus.playlist.detailsHandler = ({ itemName, options }) => { if (itemName.startsWith(`${config.sonolus.prefix}-random-`)) { const [, , , min, max] = itemName.split('-') const minRating = +(min ?? '') || 0 @@ -22,7 +23,7 @@ export const installPlaylistDetails = () => { author: databaseEngineItem.subtitle, tags: [{ title: { en: Text.Random } }], levels: randomize( - sonolus.level.items + hideSpoilers(options.spoilers, sonolus.level.items) .filter(({ rating }) => rating >= minRating && rating <= maxRating) .map(({ name }) => name), 20, @@ -30,6 +31,7 @@ export const installPlaylistDetails = () => { meta: { musicVocalTypeIndexes: new Set(), characterIndexes: new Set(), + publishedAt: Date.now(), }, }, actions: {}, @@ -43,7 +45,7 @@ export const installPlaylistDetails = () => { if (!item) return 404 return { - item, + item: hideSpoilersFromPlaylist(options.spoilers, item), description: item.description, actions: {}, hasCommunity: false, diff --git a/src/sonolus/playlist/info.ts b/src/sonolus/playlist/info.ts index 289221d..ff0679d 100644 --- a/src/sonolus/playlist/info.ts +++ b/src/sonolus/playlist/info.ts @@ -1,10 +1,15 @@ import { Icon, Text } from '@sonolus/core' import { randomize } from '../../utils/math.js' import { sonolus } from '../index.js' +import { hideSpoilersFromPlaylists } from '../utils/spoiler.js' import { playlistSearches } from './search.js' export const installPlaylistInfo = () => { - sonolus.playlist.infoHandler = () => { + sonolus.playlist.infoHandler = ({ options }) => { + const filteredPlaylists = hideSpoilersFromPlaylists( + options.spoilers, + sonolus.playlist.items, + ) return { searches: playlistSearches, sections: [ @@ -12,12 +17,12 @@ export const installPlaylistInfo = () => { title: { en: Text.Random }, icon: Icon.Shuffle, itemType: 'playlist', - items: randomize(sonolus.playlist.items, 5), + items: randomize(filteredPlaylists, 5), }, { title: { en: Text.Newest }, itemType: 'playlist', - items: sonolus.playlist.items.slice(0, 5), + items: filteredPlaylists.slice(0, 5), }, ], banner: sonolus.banner, diff --git a/src/sonolus/playlist/item.ts b/src/sonolus/playlist/item.ts index 7689507..f8fdb81 100644 --- a/src/sonolus/playlist/item.ts +++ b/src/sonolus/playlist/item.ts @@ -19,6 +19,10 @@ export const updatePlaylistItems = () => { for (const characterIndex of level.meta.characterIndexes) { playlist.meta.characterIndexes.add(characterIndex) } + + if (level.meta.publishedAt < playlist.meta.publishedAt) { + playlist.meta.publishedAt = level.meta.publishedAt + } } else { playlists.set(level.meta.musicId, { name: `${config.sonolus.prefix}-${level.meta.musicId}`, @@ -32,6 +36,7 @@ export const updatePlaylistItems = () => { meta: { musicVocalTypeIndexes: new Set([level.meta.musicVocalTypeIndex]), characterIndexes: new Set(level.meta.characterIndexes), + publishedAt: level.meta.publishedAt, }, }) } diff --git a/src/sonolus/playlist/list.ts b/src/sonolus/playlist/list.ts index 7544f4d..bb8658c 100644 --- a/src/sonolus/playlist/list.ts +++ b/src/sonolus/playlist/list.ts @@ -4,13 +4,22 @@ import { databaseEngineItem } from 'sonolus-pjsekai-engine' import { config } from '../../config.js' import { sonolus } from '../index.js' import { randomizeItems, toIndexes } from '../utils/list.js' +import { hideSpoilersFromPlaylists } from '../utils/spoiler.js' import { playlistSearches } from './search.js' export const installPlaylistList = () => { - sonolus.playlist.listHandler = ({ search: { type, options }, page }) => { + sonolus.playlist.listHandler = ({ + search: { type, options }, + page, + options: serverOptions, + }) => { + const filteredPlaylists = hideSpoilersFromPlaylists( + serverOptions.spoilers, + sonolus.playlist.items, + ) if (type === 'quick') return { - ...paginateItems(filterPlaylists(sonolus.playlist.items, options.keywords), page), + ...paginateItems(filterPlaylists(filteredPlaylists, options.keywords), page), searches: playlistSearches, } @@ -29,6 +38,7 @@ export const installPlaylistList = () => { meta: { musicVocalTypeIndexes: new Set(), characterIndexes: new Set(), + publishedAt: Date.now(), }, }, ], @@ -39,7 +49,7 @@ export const installPlaylistList = () => { const musicVocalTypeIndexes = toIndexes(options.categories) const items = filterPlaylists( - sonolus.playlist.items.filter( + filteredPlaylists.filter( ({ meta }) => (!meta.characterIndexes.size || characterIndexes.some((characterIndex) => @@ -51,7 +61,6 @@ export const installPlaylistList = () => { ), options.keywords, ) - return { ...(options.random ? randomizeItems(items) : paginateItems(items, page)), searches: playlistSearches, diff --git a/src/sonolus/playlist/model.ts b/src/sonolus/playlist/model.ts index 366f36b..b892bdb 100644 --- a/src/sonolus/playlist/model.ts +++ b/src/sonolus/playlist/model.ts @@ -4,6 +4,7 @@ declare module '@sonolus/express' { meta: { musicVocalTypeIndexes: Set characterIndexes: Set + publishedAt: number } } } diff --git a/src/sonolus/utils/spoiler.ts b/src/sonolus/utils/spoiler.ts new file mode 100644 index 0000000..0d233a3 --- /dev/null +++ b/src/sonolus/utils/spoiler.ts @@ -0,0 +1,39 @@ +import { PlaylistItemModel } from '@sonolus/express' +import { sonolus } from '../index.js' + +export const hideSpoilers = ( + passThrough: boolean, + items: T[], +): T[] => { + if (passThrough) { + return items + } + return items.filter((item) => item.meta.publishedAt <= Date.now()) +} + +export const hideSpoilersFromPlaylist = ( + passThrough: boolean, + playlist: PlaylistItemModel, +): PlaylistItemModel => { + if (passThrough) { + return playlist + } + return { + ...playlist, + levels: hideSpoilers( + false, + playlist.levels.map((levelNameOrItem) => { + if (typeof levelNameOrItem === 'object') return levelNameOrItem + const level = sonolus.level.items.find((level) => level.name === levelNameOrItem) + if (!level) throw new Error(`Level not found: ${levelNameOrItem}`) + return level + }), + ), + } +} + +export const hideSpoilersFromPlaylists = ( + passThrough: boolean, + playlists: PlaylistItemModel[], +): PlaylistItemModel[] => + playlists.map((playlist) => hideSpoilersFromPlaylist(passThrough, playlist))