diff --git a/docs/zh-CN/plugins/local.md b/docs/zh-CN/plugins/local.md index 615901cb0..287061f72 100644 --- a/docs/zh-CN/plugins/local.md +++ b/docs/zh-CN/plugins/local.md @@ -24,19 +24,29 @@ 图源文件夹,支持多个不同的文件夹 -#### languages +#### storage -- 类型: `string[]` -- 默认值: `['zh-CN', 'en']` +- 类型: `ENUM` +- 选项: `file | database` +- 默认值: `file` -图源支持的语言 +图源存数据存储方式,`file` 为文件存储,`database` 为数据库存储 -#### extension +#### reload + +- 类型: `boolean` +- 默认值: `false` + +是否在每次启动时重新扫描图源文件夹 + +#### languages - 类型: `string[]` -- 默认值: `['.jpg', '.png', '.jpeg', '.gif']` +- 默认值: `['zh-CN']` -支持的图片扩展名 +图源支持的语言 + +### 文件设置 #### scraper @@ -45,6 +55,13 @@ 文件元信息刮削器格式,详见 [刮削器](#刮削器) +#### extension + +- 类型: `string[]` +- 默认值: `['.jpg', '.png', '.jpeg', '.gif']` + +支持的图片扩展名,请注意扩展名前的 `.` 是必须的 + ## 刮削器 :::tip @@ -55,9 +72,11 @@ ### 使用 -插件设置中 `scraper` 默认值可得出大致的使用方式 +插件设置中 `scraper` 默认值可得出大致的使用方式:当 `scraper` 为 `{filename}-{tag}` 时,文件名为 `foo-[bar].jpg` 的图片将被刮削为 `{name: 'foo', tag: ['bar'], ...}`。 -### 标签 +即:文件名为 `foo` 的图片,其拥有 `bar` 这个 tags。 + +### 语法 #### `#...#` @@ -65,31 +84,33 @@ - 默认值: `name` - 示例: `#name#{fliename}-{tag}` -(仅在开头有效)指定刮削器的工作方式,目前支持以下几种方式: +> 该语法仅在 `scraper` 的第一个元素中有效,否则将被忽略 + +指定刮削器的工作方式,目前支持以下几种方式: 1. `name`: 文件名模式 -2. `meta`(WIP): 文件元信息模式(开发中) +2. `meta`: 文件元信息模式(开发中) #### `{filename}` - 类型: `string` -> 当 `{filename}` 被放置在最后时,`+` 将失效(即 `{foo}-{filename}+`) +> 当 `{filename}` 被放置在最后时,`+` 将失效(e.g. `{foo}-{filename}+`) -文件名 +指示文件名所在的位置,文件名将被刮削为 `name`,并作为图片的 `name` 属性 #### `{tag}` - 类型: `string[]` -图片拥有的 tag +指示标签所在的位置,标签将被刮削为 `tag`,并作为图片的 `tags` 属性 #### `{nsfw}`(WIP) - 类型: `boolean | 'furry' | 'guro' | 'shota' | 'bl'` - 默认值: `nsfw=false` -限制级图片 +指示图片是否为 nsfw,若为 `boolean` 类型,则直接将其作为 `nsfw` 属性,若为 `string` 类型,则将其作为 `nsfw` 属性的值 #### `+` diff --git a/packages/local/src/index.ts b/packages/local/src/index.ts index a4df0dcfa..6d160ef06 100644 --- a/packages/local/src/index.ts +++ b/packages/local/src/index.ts @@ -16,6 +16,10 @@ declare module 'koishi' { } class LocalImageSource extends ImageSource { + static override inject = { + required: ['booru'], + optional: ['database', 'cache'] + } languages = [] source = 'local' private imageMap: LocalStorage.Type[] = [] @@ -26,7 +30,7 @@ class LocalImageSource extends ImageSource { this.languages = config.languages this.logger = ctx.logger('booru-local') - if (this.config.storage === 'database') this.ctx.using(['database'], async (ctx, options) => { + if (config.storage === 'database') ctx.using(['database'], async (ctx) => { ctx.model.extend('booru_local', { storeId: 'string', storeName: 'text', @@ -42,7 +46,7 @@ class LocalImageSource extends ImageSource { this.imageMap = await ctx.database.get('booru_local', {}) }) - if (this.config.storage === 'file') { + if (config.storage === 'file') { const absMap = resolve(ctx.root.baseDir, LocalImageSource.DataDir, LocalImageSource.RootMap) if (!existsSync(resolve(ctx.root.baseDir, LocalImageSource.DataDir))) mkdirs(resolve(ctx.root.baseDir, LocalImageSource.DataDir)) @@ -61,7 +65,7 @@ class LocalImageSource extends ImageSource { } // TODO: cache storage - if (this.config.storage === 'cache') this.ctx.using(['cache'], () => { }) + if (config.storage === 'cache') ctx.using(['cache'], () => { }) ctx.on('ready', async () => { if (config.endpoint.length <= 0) return this.logger.warn('no folder yet') @@ -72,6 +76,8 @@ class LocalImageSource extends ImageSource { images: 0 } this.logger.info('Initializing storages...') + // duplicate check + this.config.endpoint = [...new Set(this.config.endpoint)] if (this.imageMap.length > 0) mapping = mapping.update(this.imageMap) // mapping the folders to memory by loop for await (let path of config.endpoint) { @@ -102,11 +108,34 @@ class LocalImageSource extends ImageSource { async get(query: ImageSource.Query): Promise { if (this.imageMap.length < 1) return undefined - const map = this.imageMap.length === 1 ? this.imageMap[0] : Random.pick(this.imageMap) - if (query.tags.length > 0) { - map.images = map.images.filter(img => [...new Set([...img.tags, ...query.tags])].length > 0) + let pickPool = []; + // Flatten all maps + if (this.imageMap.length > 1) { + for (const storage of this.imageMap) { + if (query.tags.length > 0) { + // filter by tags + for (const image of storage.images) { + if (query.tags.every(tag => image.tags.includes(tag))) + pickPool.push(image) + } + } else { + // pick from all images + pickPool.push(...storage.images) + } + } + } else { + // pick from one image map + pickPool = this.imageMap.map((storage) => { + if (query.tags.length > 0) { + // filter by tags + return storage.images.filter((image) => query.tags.every(tag => image.tags.includes(tag))) + } else { + // pick from all images + return storage.images + } + }).flat() } - const picker = Random.pick(map.images, query.count) + const picker = Random.pick(pickPool, query.count) return picker.map(img => { return { url: pathToFileURL(img.path).href, @@ -137,7 +166,7 @@ namespace LocalImageSource { // TODO: Schema.path()? endpoint: Schema.array(String).description('图源文件夹,支持多个不同的文件夹'), storage: Schema.union(['file', 'database']).description('图源数据保存方式').default('file'), - reload: Schema.boolean().description('每次启动时重新加载所有图片').default(false), + reload: Schema.boolean().description('每次启动时重新构建图源数据').default(false), languages: Schema.array(String).description('支持的语言').default(['zh-CN']) }).description('图源设置'), Schema.object({ diff --git a/packages/local/src/scraper.ts b/packages/local/src/scraper.ts index d38aeef80..7ac16fa75 100644 --- a/packages/local/src/scraper.ts +++ b/packages/local/src/scraper.ts @@ -6,13 +6,13 @@ const element = { tag: '(\\[.+\\])', } -const nfsw = [true, false, 'furry', 'guro', 'shota', 'bl'] -type Nfsw = boolean | 'furry' | 'guro' | 'shota' | 'bl' +const nsfw = [true, false, 'furry', 'guro', 'shota', 'bl'] +type Nsfw = boolean | 'furry' | 'guro' | 'shota' | 'bl' const format = { filename: (name: string) => name, tag: (tags: string) => tags.slice(1, -1).replace(',', ',').split(',').map(s => s.trim()), - nfsw: (tag: string) => nfsw.includes(tag.split('=')[1]) + nsfw: (tag: string) => nsfw.includes(tag.split('=')[1]) } const mapping = {