├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── api-spec └── server │ ├── openapi.yaml │ ├── pop │ └── record.yaml │ ├── root.yaml │ └── waifu │ ├── list.yaml │ └── list │ └── popcount.yaml ├── build-web.sh ├── config ├── apiSpecPath.js ├── env.js ├── ipTrustConfig.js ├── limitConfig.js ├── mongodbConfig.js ├── package.json └── webFilePath.js ├── dev-script ├── insertWaifu.js ├── syncIndex.js └── updateSchema.js ├── ecosystem.cron.config.js ├── ecosystem.server.config.js ├── package-lock.json ├── package.json └── src ├── common ├── checker │ ├── id.js │ └── object.js ├── connection │ ├── Mongodb.js │ └── redis.js ├── error │ ├── HttpError.js │ └── consoleUnexpectedError.js ├── package.json └── utils │ ├── BackgroundRunner.js │ └── sleep.js ├── cron └── main.js ├── entity ├── package.json ├── popLog │ ├── PopLog.js │ ├── PopLogBridge.js │ ├── PopLogRepo.js │ └── helper.js └── waifu │ ├── Waifu.js │ ├── WaifuBridge.js │ ├── WaifuModel.js │ ├── WaifuRepo.js │ ├── helper.js │ └── waifuRepoMethod │ └── addWaifusPopCount.js └── server ├── controller ├── pop │ ├── PopController.js │ └── recordPop.js ├── waifu │ ├── WaifuController.js │ ├── getList.js │ └── getPopCountList.js └── webClient │ ├── WebClientController.js │ ├── normalPageHead.js │ └── waifuPopPageHead.js ├── expressApp.js ├── main.js ├── package.json └── router ├── apiRouter.js ├── apiSpecRouter.js ├── getIp.js ├── getReqHandleFunc.js ├── popRouter.js ├── waifuRouter.js └── webClientRouter.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | popwaifu-web/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true 6 | }, 7 | extends: [ 8 | 'standard', 9 | 'plugin:yml/standard' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 'latest' 13 | }, 14 | rules: { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "popwaifu-web"] 2 | path = popwaifu-web 3 | url = https://github.com/SoftwareSing/popwaifu-web.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SoftwareSing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # popwaifu 2 | 3 | Here is [popwaifu.click](https://popwaifu.click/) backend project. 4 | Frontend project is on [popwaifu-web](https://github.com/SoftwareSing/popwaifu-web) 5 | 6 | ## run server 7 | 8 | 1. Install Node.js 9 | I use 14.17.6 when writing this, recommend using [NVM](https://github.com/nvm-sh/nvm) to install Node.js 10 | 2. Install MongoDB and Redis 11 | 3. git clone this project 12 | 4. run `npm install` 13 | 5. run `npm run build_web` to build frontend file 14 | 6. run `npm run server_dev` to start a develop server 15 | 16 | ## file architecture 17 | 18 | - [config](/config) folder put some config file, you can change your MongoDB connection config here 19 | - [dev-script](/dev-script) folder put some script help develop, you can use [insertWaifu.js](/dev-script/insertWaifu.js) to quickly put some waifu into your DB 20 | - [popwaifu-web](/popwaifu-web) folder is a git submodule folder, it link to [popwaifu-web](https://github.com/SoftwareSing/popwaifu-web) project 21 | - [src](/src) folder is our source code folder 22 | 23 | ### src 24 | 25 | I try to follow [this video](https://youtu.be/gX5oB4fgX6U?t=2568)'s architecture 26 | 27 | [entity](/src/entity) is the basic object, you can use repository object to get a entity object. 28 | For example, you can use [WaifuRepo.getByUrlId()](/src/entity/waifu/WaifuRepo.js) to get a [Waifu](/src/entity/waifu/Waifu.js), and [WaifuRepo](/src/entity/waifu/WaifuRepo.js)'s methods should be the only way how you get this entity. 29 | -------------------------------------------------------------------------------- /api-spec/server/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: popwaifu API 4 | version: 0.0.1 5 | servers: 6 | - url: /api 7 | - url: "https://popwaifu.click/api" 8 | paths: 9 | /: 10 | $ref: ./root.yaml 11 | # pop 12 | /v1/pop/record: 13 | $ref: ./pop/record.yaml 14 | # waifu 15 | /v1/waifu/list: 16 | $ref: ./waifu/list.yaml 17 | /v1/waifu/list/popcount: 18 | $ref: ./waifu/list/popcount.yaml 19 | -------------------------------------------------------------------------------- /api-spec/server/pop/record.yaml: -------------------------------------------------------------------------------- 1 | post: 2 | operationId: pop.record.post.v1 3 | summary: record clicks 4 | tags: 5 | - pop 6 | requestBody: 7 | required: true 8 | content: 9 | application/json: 10 | schema: 11 | type: object 12 | properties: 13 | waifuPopObj: 14 | required: true 15 | type: object 16 | additionalProperties: 17 | description: key is waifu id, and value is click count 18 | type: integer 19 | minimum: 1 20 | maximum: 150 21 | example: 22 | 612ced2ee6bef19361a26a27: 100 23 | 613e0d553097fa58dcfc9295: 50 24 | responses: 25 | "202": 26 | description: success record response 27 | "429": 28 | description: too many request 29 | -------------------------------------------------------------------------------- /api-spec/server/root.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | operationId: root.get 3 | summary: for testing api server is online 4 | responses: 5 | "200": 6 | description: normal response 7 | content: 8 | application/json: 9 | schema: 10 | type: string 11 | example: popwaifu API 12 | -------------------------------------------------------------------------------- /api-spec/server/waifu/list.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | operationId: waifu.list.get.v1 3 | summary: get all waifu info 4 | tags: 5 | - waifu 6 | responses: 7 | "200": 8 | description: waifu list 9 | content: 10 | application/json: 11 | schema: 12 | type: array 13 | items: 14 | type: object 15 | additionalProperties: false 16 | properties: 17 | waifuId: 18 | description: waifu id 19 | required: true 20 | type: string 21 | example: 612ced2ee6bef19361a26a2b 22 | urlId: 23 | description: url path 24 | required: true 25 | type: string 26 | example: miku 27 | name: 28 | description: waifu name 29 | required: true 30 | type: string 31 | example: 初音ミク 32 | popCount: 33 | description: click count 34 | required: true 35 | type: integer 36 | modeConfigList: 37 | description: waifu can have many set of image config, every waifu has a default mode which modeName is `default` 38 | required: true 39 | type: array 40 | items: 41 | type: object 42 | additionalProperties: false 43 | properties: 44 | modeName: 45 | required: true 46 | type: string 47 | example: default 48 | imgNormalUrl: 49 | required: true 50 | type: string 51 | imgPopUrl: 52 | required: true 53 | type: string 54 | imgIconUrl: 55 | required: true 56 | type: string 57 | imgInfo: 58 | required: true 59 | type: string 60 | audioNormalUrl: 61 | required: true 62 | type: string 63 | audioPopUrl: 64 | required: true 65 | type: string 66 | audioInfo: 67 | required: true 68 | type: string 69 | -------------------------------------------------------------------------------- /api-spec/server/waifu/list/popcount.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | operationId: waifu.list.popcount.get.v1 3 | summary: get all waifu click count info 4 | tags: 5 | - waifu 6 | responses: 7 | "200": 8 | description: waifu list 9 | content: 10 | application/json: 11 | schema: 12 | type: array 13 | items: 14 | type: object 15 | additionalProperties: false 16 | properties: 17 | waifuId: 18 | description: waifu id 19 | required: true 20 | type: string 21 | example: 612ced2ee6bef19361a26a2b 22 | popCount: 23 | description: click count 24 | required: true 25 | type: integer 26 | -------------------------------------------------------------------------------- /build-web.sh: -------------------------------------------------------------------------------- 1 | git submodule update --init --recursive 2 | cd ./popwaifu-web 3 | npm i 4 | npm run build 5 | cd .. 6 | -------------------------------------------------------------------------------- /config/apiSpecPath.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const apiSpecDirPath = path.resolve(__dirname, '..', 'api-spec') 4 | 5 | const serverApiSpecDirPath = path.resolve(apiSpecDirPath, 'server') 6 | const serverApiSpecMainPath = path.resolve(serverApiSpecDirPath, 'openapi.yaml') 7 | 8 | module.exports = { 9 | apiSpecDirPath, 10 | serverApiSpecDirPath, 11 | serverApiSpecMainPath 12 | } 13 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | const ENV = process.env.NODE_ENV 2 | 3 | const development = 'development' 4 | const production = 'production' 5 | 6 | module.exports = { 7 | ENV, 8 | envKeyword: { 9 | development, 10 | production 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/ipTrustConfig.js: -------------------------------------------------------------------------------- 1 | exports.ipv4Trust = Object.freeze([ 2 | '10.0.0.0/8', 3 | '172.16.0.0/12', 4 | '192.168.0.0/16', 5 | '127.0.0.1/32' 6 | ]) 7 | 8 | exports.ipv6Trust = Object.freeze([ 9 | 'fc00::/7', 10 | '::1/128', 11 | 'fe80::/10' 12 | ]) 13 | -------------------------------------------------------------------------------- /config/limitConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | popLimit: { 3 | time: 5 * 1000, 4 | count: 150 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /config/mongodbConfig.js: -------------------------------------------------------------------------------- 1 | const { ENV, envKeyword } = require('./env') 2 | 3 | module.exports = mongodbConfig(ENV) 4 | 5 | function mongodbConfig (env) { 6 | switch (env) { 7 | case envKeyword.production: { 8 | return { 9 | mongoUrl: 'mongodb://127.0.0.1:27017/popwaifu' 10 | } 11 | } 12 | default: { 13 | return { 14 | mongoUrl: 'mongodb://127.0.0.1:27017/popwaifu' 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "~config", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /config/webFilePath.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const webDirPath = path.resolve(__dirname, '..', 'popwaifu-web') 4 | const webPublicDirPath = path.resolve(webDirPath, 'public') 5 | 6 | module.exports = { 7 | webPublicDirPath, 8 | webIndexFilePath: path.resolve(webPublicDirPath, 'index.html'), 9 | webPageHeadFilePath: path.resolve(webDirPath, 'src', 'PageHead.svelte') 10 | } 11 | -------------------------------------------------------------------------------- /dev-script/insertWaifu.js: -------------------------------------------------------------------------------- 1 | const { disconnectRedis } = require('~common/connection/redis') 2 | const Mongodb = require('~common/connection/Mongodb') 3 | const WaifuRepo = require('~entity/waifu/WaifuRepo') 4 | 5 | async function run () { 6 | await Mongodb.connect() 7 | await insertWaifu() 8 | await Mongodb.disconnect() 9 | await disconnectRedis() 10 | } 11 | 12 | async function insertWaifu () { 13 | await WaifuRepo.upsertWaifu({ 14 | urlId: 'no15', 15 | name: '十五號', 16 | modeConfigList: [ 17 | { 18 | modeName: 'default', 19 | imgNormalUrl: 'https://i.imgur.com/yYmR0t5.png', 20 | imgPopUrl: 'https://i.imgur.com/cmFYQri.png', 21 | imgIconUrl: 'https://i.imgur.com/Qq6Cp1U.png', 22 | imgInfo: 'picture from PopLeopardCat ( https://no15rescute.github.io/PopLeopardCat/ )', 23 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/15-a.mp3', 24 | audioInfo: 'audio from PopLeopardCat ( https://no15rescute.github.io/PopLeopardCat/ )' 25 | }, 26 | { 27 | modeName: 'one-two', 28 | imgNormalUrl: 'https://i.imgur.com/0rFGRui.png', 29 | imgPopUrl: 'https://i.imgur.com/GsxuGZN.png', 30 | imgIconUrl: 'https://i.imgur.com/qi8osxh.png', 31 | imgInfo: 'https://youtu.be/QEQntL4Bb14', 32 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/15-two.mp3', 33 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/15-one.mp3', 34 | audioInfo: 'https://youtu.be/QEQntL4Bb14' 35 | }, 36 | { 37 | modeName: 'no150', 38 | imgNormalUrl: 'https://i.imgur.com/Sh5gP3C.png', 39 | imgPopUrl: 'https://i.imgur.com/bPgAvRY.png', 40 | imgIconUrl: 'https://i.imgur.com/RZuyVHs.png', 41 | imgInfo: 'https://youtu.be/J4kvgE3bEPA?t=6452', 42 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/15-150-2.mp3', 43 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/15-150-1.mp3', 44 | audioInfo: 'https://youtu.be/J4kvgE3bEPA?t=6452' 45 | }, 46 | { 47 | modeName: 'chicken', 48 | imgNormalUrl: 'https://i.imgur.com/V4izKux.png', 49 | imgPopUrl: 'https://i.imgur.com/imqEcWP.png', 50 | imgIconUrl: 'https://i.imgur.com/yAUW1Xa.png', 51 | imgInfo: 'https://youtu.be/sR6Lh05jjLE?t=8115', 52 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/15-chicken.mp3', 53 | audioInfo: 'https://youtu.be/sR6Lh05jjLE?t=8115' 54 | } 55 | ] 56 | }) 57 | await WaifuRepo.upsertWaifu({ 58 | urlId: 'annin-miru', 59 | name: '杏仁ミル', 60 | modeConfigList: [ 61 | { 62 | modeName: 'default', 63 | imgNormalUrl: 'https://i.imgur.com/yWz4gP1.png', 64 | imgPopUrl: 'https://i.imgur.com/P5DjSxE.png', 65 | imgIconUrl: 'https://i.imgur.com/UHPLtzv.png', 66 | imgInfo: 'picture from 杏仁ミル twitter ( https://twitter.com/AnninMirudayo/status/1422580442876715008 ) ( https://twitter.com/AnninMirudayo/status/1421945176872742913 )', 67 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/miru-a-2.mp3', 68 | audioInfo: 'https://youtu.be/19sRmPz6HIY?t=22' 69 | } 70 | ] 71 | }) 72 | await WaifuRepo.upsertWaifu({ 73 | urlId: 'miku', 74 | name: '初音ミク', 75 | modeConfigList: [ 76 | { 77 | modeName: 'default', 78 | imgNormalUrl: 'https://i.imgur.com/w50ILOk.jpg', 79 | imgPopUrl: 'https://i.imgur.com/8jOQjgL.jpg', 80 | imgIconUrl: 'https://i.imgur.com/meOjXSc.png', 81 | imgInfo: 'picture from pixiv: 千夜QYS3 ( https://www.pixiv.net/artworks/56710319 )', 82 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/miku-meow-2.mp3', 83 | audioInfo: 'https://youtu.be/kS2yWmLCtnc?t=44' 84 | } 85 | ] 86 | }) 87 | await WaifuRepo.upsertWaifu({ 88 | urlId: 'ubye', 89 | name: '悠白', 90 | modeConfigList: [ 91 | { 92 | modeName: 'default', 93 | imgNormalUrl: 'https://i.imgur.com/yO9r0QX.png', 94 | imgPopUrl: 'https://i.imgur.com/1QCbJpr.png', 95 | imgIconUrl: 'https://i.imgur.com/aNbJ5eV.png', 96 | imgInfo: 'https://youtu.be/jXW6zlzLCGg?t=3585', 97 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/ubye-a.mp3', 98 | audioInfo: 'https://youtu.be/kA6oIG9ulVs?t=3' 99 | } 100 | ] 101 | }) 102 | await WaifuRepo.upsertWaifu({ 103 | urlId: 'gura', 104 | name: 'Gawr Gura', 105 | modeConfigList: [ 106 | { 107 | modeName: 'default', 108 | imgNormalUrl: 'https://i.imgur.com/G47Ea40.png', 109 | imgPopUrl: 'https://i.imgur.com/tly5u3M.png', 110 | imgIconUrl: 'https://i.imgur.com/bGuWlhS.png', 111 | imgInfo: 'https://youtu.be/dBK0gKW61NU?t=213', 112 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/gura-a.mp3', 113 | audioInfo: 'https://youtu.be/dBK0gKW61NU?t=220' 114 | } 115 | ] 116 | }) 117 | await WaifuRepo.upsertWaifu({ 118 | urlId: 'shirakami-fubuki', 119 | name: '白上フブキ', 120 | modeConfigList: [ 121 | { 122 | modeName: 'default', 123 | imgNormalUrl: 'https://i.imgur.com/1dXI4bn.png', 124 | imgPopUrl: 'https://i.imgur.com/ecFG33p.png', 125 | imgIconUrl: 'https://i.imgur.com/jvtlEsd.png', 126 | imgInfo: 'https://en.hololive.tv/portfolio/items/shirakami-fubuki', 127 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/shirakami-fubuki-nya.mp3', 128 | audioInfo: 'https://youtu.be/kQXc80jgk-E?t=7654' 129 | } 130 | ] 131 | }) 132 | await WaifuRepo.upsertWaifu({ 133 | urlId: 'dio', 134 | name: 'DIO', 135 | modeConfigList: [ 136 | { 137 | modeName: 'default', 138 | imgNormalUrl: 'https://i.imgur.com/tdCsOD3.png', 139 | imgPopUrl: 'https://i.imgur.com/RCpRHu6.png', 140 | imgIconUrl: 'https://i.imgur.com/tdCsOD3.png', 141 | imgInfo: 'unknown, but seems from JoJo Part 3' 142 | } 143 | ] 144 | }) 145 | await WaifuRepo.upsertWaifu({ 146 | urlId: 'coco', 147 | name: '桐生ココ', 148 | modeConfigList: [ 149 | { 150 | modeName: 'default', 151 | imgNormalUrl: 'https://i.imgur.com/bydcGFC.png', 152 | imgPopUrl: 'https://i.imgur.com/kdJyMuB.png', 153 | imgIconUrl: 'https://i.imgur.com/kdJyMuB.png', 154 | imgInfo: 'https://virtualyoutuber.fandom.com/wiki/Kiryu_Coco/Gallery', 155 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/coco-next-meme.mp3', 156 | audioInfo: 'https://youtu.be/-AGhA-GaZ_o?t=25' 157 | } 158 | ] 159 | }) 160 | await WaifuRepo.upsertWaifu({ 161 | urlId: 'minato-aqua', 162 | name: '湊あくあ', 163 | modeConfigList: [ 164 | { 165 | modeName: 'default', 166 | imgNormalUrl: 'https://i.imgur.com/9Xdi8O1.jpg', 167 | imgPopUrl: 'https://i.imgur.com/lvB1h1i.jpg', 168 | imgIconUrl: 'https://i.imgur.com/poPJZ8S.png', 169 | imgInfo: 'https://youtu.be/rddmVGgem2Q', 170 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/minato-aqua-nya.mp3', 171 | audioInfo: 'https://youtu.be/rddmVGgem2Q' 172 | } 173 | ] 174 | }) 175 | await WaifuRepo.upsertWaifu({ 176 | urlId: 'rayer', 177 | name: '蕾兒', 178 | modeConfigList: [ 179 | { 180 | modeName: 'default', 181 | imgNormalUrl: 'https://i.imgur.com/XM6yUUU.png', 182 | imgPopUrl: 'https://i.imgur.com/zIBcFwy.png', 183 | imgIconUrl: 'https://i.imgur.com/zIBcFwy.png', 184 | imgInfo: 'https://youtu.be/W9xiniyF2Zk', 185 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/rayer-pop.mp3', 186 | audioInfo: 'https://youtu.be/W9xiniyF2Zk' 187 | } 188 | ] 189 | }) 190 | await WaifuRepo.upsertWaifu({ 191 | urlId: 'padko', 192 | name: '平平子', 193 | modeConfigList: [ 194 | { 195 | modeName: 'default', 196 | imgNormalUrl: 'https://i.imgur.com/3FBEBVL.jpg', 197 | imgPopUrl: 'https://i.imgur.com/VQEWET6.jpg', 198 | imgIconUrl: 'https://i.imgur.com/xrypgmh.png', 199 | imgInfo: 'https://youtu.be/Z4iRCaUsQx0', 200 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/padko-1.mp3', 201 | audioInfo: 'https://youtu.be/BjOQD6j9Zyc?t=9233' 202 | } 203 | ] 204 | }) 205 | await WaifuRepo.upsertWaifu({ 206 | urlId: 'gojo-masaru', 207 | name: '五条勝', 208 | modeConfigList: [ 209 | { 210 | modeName: 'default', 211 | imgNormalUrl: 'https://i.imgur.com/t8MR5ju.png', 212 | imgPopUrl: 'https://i.imgur.com/Sxlf2fU.png', 213 | imgIconUrl: 'https://i.imgur.com/AeNdaD2.png', 214 | imgInfo: 'https://youtu.be/K9_A35uCpYA' 215 | } 216 | ] 217 | }) 218 | await WaifuRepo.upsertWaifu({ 219 | urlId: 'beta-hoonie', 220 | name: 'β虎妮', 221 | modeConfigList: [ 222 | { 223 | modeName: 'default', 224 | imgNormalUrl: 'https://i.imgur.com/ZcnpKP1.png', 225 | imgPopUrl: 'https://i.imgur.com/FCasd4b.png', 226 | imgIconUrl: 'https://i.imgur.com/fWlFr6q.png', 227 | imgInfo: 'https://youtu.be/8DepX5_8k2E', 228 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/beta-hoonie-pop.mp3', 229 | audioInfo: 'https://youtu.be/8DepX5_8k2E' 230 | } 231 | ] 232 | }) 233 | await WaifuRepo.upsertWaifu({ 234 | urlId: 'hu-tao', 235 | name: '胡桃', 236 | modeConfigList: [ 237 | { 238 | modeName: 'default', 239 | imgNormalUrl: 'https://i.imgur.com/BzcRfrd.jpg', 240 | imgPopUrl: 'https://i.imgur.com/NB3Zaze.jpg', 241 | imgIconUrl: 'https://i.imgur.com/ArXQXwF.png', 242 | imgInfo: 'https://www.pixiv.net/artworks/92049927', 243 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/hu-tao-pop.mp3', 244 | audioInfo: 'from Genshin Impact PC' 245 | } 246 | ] 247 | }) 248 | await WaifuRepo.upsertWaifu({ 249 | urlId: 'lancee', 250 | name: '蘭希LanCee', 251 | modeConfigList: [ 252 | { 253 | modeName: 'default', 254 | imgNormalUrl: 'https://i.imgur.com/Hb7eDiq.png', 255 | imgPopUrl: 'https://i.imgur.com/Wmt52Vf.png', 256 | imgIconUrl: 'https://i.imgur.com/j4osERO.png', 257 | imgInfo: 'https://www.youtube.com/watch?v=M7Ym_4ngeT8&t=2387s', 258 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/lancee-pop.mp3', 259 | audioInfo: 'https://www.youtube.com/watch?v=M7Ym_4ngeT8&t=2387s' 260 | } 261 | ] 262 | }) 263 | await WaifuRepo.upsertWaifu({ 264 | urlId: 'ayame', 265 | name: '百鬼あやめ', 266 | modeConfigList: [ 267 | { 268 | modeName: 'default', 269 | imgNormalUrl: 'https://i.imgur.com/fnkYIDH.jpg', 270 | imgPopUrl: 'https://i.imgur.com/gKremaa.jpg', 271 | imgIconUrl: 'https://i.imgur.com/8tZit6E.png', 272 | imgInfo: 'https://youtu.be/05VuFmvHjNY', 273 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/ayame-yodazo.mp3', 274 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/ayame-yodayo.mp3', 275 | audioInfo: 'https://youtu.be/05VuFmvHjNY & https://youtu.be/hlH6iNcEvq8' 276 | } 277 | ] 278 | }) 279 | await WaifuRepo.upsertWaifu({ 280 | urlId: 'patra', 281 | name: '周防パトラ', 282 | modeConfigList: [ 283 | { 284 | modeName: 'default', 285 | imgNormalUrl: 'https://i.imgur.com/fngi9Il.png', 286 | imgPopUrl: 'https://i.imgur.com/H8J0BNy.png', 287 | imgIconUrl: 'https://i.imgur.com/waHRKCT.png', 288 | imgInfo: 'https://twitter.com/Patra_HNST/status/1431010128438784003', 289 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/patra-pop.mp3', 290 | audioInfo: 'https://twitter.com/Patra_HNST/status/1429850594504679436' 291 | } 292 | ] 293 | }) 294 | await WaifuRepo.upsertWaifu({ 295 | urlId: 'lumina', 296 | name: 'Lumina', 297 | modeConfigList: [ 298 | { 299 | modeName: 'default', 300 | imgNormalUrl: 'https://i.imgur.com/0Yx8d1g.png', 301 | imgPopUrl: 'https://i.imgur.com/nyVDYir.png', 302 | imgIconUrl: 'https://i.imgur.com/QVVwFXT.png', 303 | imgInfo: 'https://youtu.be/1c2gGgQbbOE?t=207', 304 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/lumina-pop.mp3', 305 | audioInfo: 'https://youtu.be/1c2gGgQbbOE?t=207' 306 | } 307 | ] 308 | }) 309 | await WaifuRepo.upsertWaifu({ 310 | urlId: 'boru', 311 | name: '毬庫波爾', 312 | modeConfigList: [ 313 | { 314 | modeName: 'default', 315 | imgNormalUrl: 'https://i.imgur.com/ecqpAXe.png', 316 | imgPopUrl: 'https://i.imgur.com/YyJTGT0.png', 317 | imgIconUrl: 'https://i.imgur.com/b4tgOmH.png', 318 | imgInfo: 'https://youtu.be/5cQn-G3PGMA', 319 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/boru-pop.mp3', 320 | audioInfo: 'from herself' 321 | } 322 | ] 323 | }) 324 | await WaifuRepo.upsertWaifu({ 325 | urlId: 'ryoko724', 326 | name: '神崎涼子', 327 | modeConfigList: [ 328 | { 329 | modeName: 'default', 330 | imgNormalUrl: 'https://i.imgur.com/ekSYWDo.png', 331 | imgPopUrl: 'https://i.imgur.com/EhTSOxs.png', 332 | imgIconUrl: 'https://i.imgur.com/OAYNn7a.png', 333 | imgInfo: 'from herself', 334 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/ryoko724-pop.mp3', 335 | audioInfo: 'from herself' 336 | } 337 | ] 338 | }) 339 | await WaifuRepo.upsertWaifu({ 340 | urlId: 'catastrophe', 341 | name: '達克卡塔史託洛福', 342 | modeConfigList: [ 343 | { 344 | modeName: 'default', 345 | imgNormalUrl: 'https://i.imgur.com/6nTxyZK.png', 346 | imgPopUrl: 'https://i.imgur.com/D8oG9Jv.png', 347 | imgIconUrl: 'https://i.imgur.com/1hU8uXK.png', 348 | imgInfo: 'from himself', 349 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/catastrophe-pop.mp3', 350 | audioInfo: 'from himself' 351 | } 352 | ] 353 | }) 354 | await WaifuRepo.upsertWaifu({ 355 | urlId: 'erokawasaya', 356 | name: '榎川幸', 357 | modeConfigList: [ 358 | { 359 | modeName: 'default', 360 | imgNormalUrl: 'https://i.imgur.com/egDxILb.jpeg', 361 | imgPopUrl: 'https://i.imgur.com/XtcU0cz.jpeg', 362 | imgIconUrl: 'https://i.imgur.com/aUKEu1N.png', 363 | imgInfo: 'https://youtu.be/ZYY0Wq5NeE4?t=440', 364 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/erokawasaya-pop.mp3', 365 | audioInfo: 'https://youtu.be/ZYY0Wq5NeE4?t=440' 366 | } 367 | ] 368 | }) 369 | await WaifuRepo.upsertWaifu({ 370 | urlId: 'bianfubeite', 371 | name: '蝙蝠貝特', 372 | modeConfigList: [ 373 | { 374 | modeName: 'default', 375 | imgNormalUrl: 'https://i.imgur.com/HfvXmUV.jpg', 376 | imgPopUrl: 'https://i.imgur.com/EoQhb1T.jpg', 377 | imgIconUrl: 'https://i.imgur.com/BXu3atD.png', 378 | imgInfo: 'from herself https://www.facebook.com/profile.php?id=100072528154937', 379 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/bianfubeite-pop.mp3', 380 | audioInfo: 'from herself' 381 | } 382 | ] 383 | }) 384 | await WaifuRepo.upsertWaifu({ 385 | urlId: 'merak', 386 | name: '天璇', 387 | modeConfigList: [ 388 | { 389 | modeName: 'default', 390 | imgNormalUrl: 'https://i.imgur.com/KbZMBUV.png', 391 | imgPopUrl: 'https://i.imgur.com/8qrf6qJ.png', 392 | imgIconUrl: 'https://i.imgur.com/8qrf6qJ.png', 393 | imgInfo: 'from herself', 394 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/merak-pop.mp3', 395 | audioInfo: 'from herself' 396 | } 397 | ] 398 | }) 399 | await WaifuRepo.upsertWaifu({ 400 | urlId: 'hasukiaoi', 401 | name: '狛井葵', 402 | modeConfigList: [ 403 | { 404 | modeName: 'default', 405 | imgNormalUrl: 'https://i.imgur.com/K8x1fuM.png', 406 | imgPopUrl: 'https://i.imgur.com/sbfjOAl.png', 407 | imgIconUrl: 'https://i.imgur.com/3pcbucZ.png', 408 | imgInfo: 'from her discord server', 409 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/hasukiaoi-pop.mp3', 410 | audioInfo: 'https://www.youtube.com/shorts/YUP6woQDBDc' 411 | }, 412 | { 413 | modeName: 'black', 414 | imgNormalUrl: 'https://i.imgur.com/6vtP85m.png', 415 | imgPopUrl: 'https://i.imgur.com/IwEQa50.png', 416 | imgIconUrl: 'https://i.imgur.com/rrIerIu.png', 417 | imgInfo: 'from her discord server', 418 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/hasukiaoi-black-pop.mp3', 419 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/hasukiaoi-black-normal.mp3', 420 | audioInfo: 'https://youtu.be/DRSii5MqqD8?t=1251' 421 | } 422 | ] 423 | }) 424 | await WaifuRepo.upsertWaifu({ 425 | urlId: 'aisu', 426 | name: '小林あいす', 427 | modeConfigList: [ 428 | { 429 | modeName: 'default', 430 | imgNormalUrl: 'https://i.imgur.com/vmoBTtM.jpg', 431 | imgPopUrl: 'https://i.imgur.com/Wajbupw.png', 432 | imgIconUrl: 'https://i.imgur.com/Fz0RDlW.png', 433 | imgInfo: 'from https://twitter.com/kobayashi_aisu/status/1412802879358787592 & https://youtu.be/kiKTbtsywbs?t=4050', 434 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/aisu-pop.mp3', 435 | audioInfo: 'https://youtu.be/tY9tPscDBVo' 436 | } 437 | ] 438 | }) 439 | await WaifuRepo.upsertWaifu({ 440 | urlId: 'jotaro', 441 | name: '空条承太郎', 442 | modeConfigList: [ 443 | { 444 | modeName: 'default', 445 | imgNormalUrl: 'https://i.imgur.com/iETbuuj.png', 446 | imgPopUrl: 'https://i.imgur.com/kYvJ3LN.png', 447 | imgIconUrl: 'https://i.imgur.com/12pGYEJ.png', 448 | imgInfo: 'https://youtu.be/M4OT_zxvcLc?t=22', 449 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/jotaro-yakamashi.mp3', 450 | audioInfo: 'https://youtu.be/M4OT_zxvcLc?t=22' 451 | } 452 | ] 453 | }) 454 | await WaifuRepo.upsertWaifu({ 455 | urlId: 'tcharuru', 456 | name: '天晴Haruru', 457 | modeConfigList: [ 458 | { 459 | modeName: 'default', 460 | imgNormalUrl: 'https://i.imgur.com/ZiNYFL4.png', 461 | imgPopUrl: 'https://i.imgur.com/ZWmZAIp.png', 462 | imgIconUrl: 'https://i.imgur.com/GpS1irg.png', 463 | imgInfo: 'from himself', 464 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/tcharuru-pop.mp3', 465 | audioInfo: 'from himself' 466 | } 467 | ] 468 | }) 469 | await WaifuRepo.upsertWaifu({ 470 | urlId: 'moritsukireiyi', 471 | name: '森月澪依', 472 | modeConfigList: [ 473 | { 474 | modeName: 'default', 475 | imgNormalUrl: 'https://i.imgur.com/cRRQADh.png', 476 | imgPopUrl: 'https://i.imgur.com/LwWwier.png', 477 | imgIconUrl: 'https://i.imgur.com/0aNcCOG.png', 478 | imgInfo: 'https://youtu.be/Oh7zaw108g8', 479 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/moritsukireiyi-pop.mp3', 480 | audioInfo: 'https://youtu.be/Oh7zaw108g8?t=1' 481 | } 482 | ] 483 | }) 484 | await WaifuRepo.upsertWaifu({ 485 | urlId: 'uto', 486 | name: '天使うと', 487 | modeConfigList: [ 488 | { 489 | modeName: 'default', 490 | imgNormalUrl: 'https://i.imgur.com/DpoiSoe.png', 491 | imgPopUrl: 'https://i.imgur.com/Ivy7P4o.png', 492 | imgIconUrl: 'https://i.imgur.com/DpoiSoe.png', 493 | imgInfo: 'https://youtu.be/mH3JaxvyNj4', 494 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/uto-pop.mp3', 495 | audioInfo: 'from her stream (not sure which time)' 496 | } 497 | ] 498 | }) 499 | await WaifuRepo.upsertWaifu({ 500 | urlId: 'emerald', 501 | name: 'Emerald', 502 | modeConfigList: [ 503 | { 504 | modeName: 'default', 505 | imgNormalUrl: 'https://i.imgur.com/WcJL5Um.png', 506 | imgPopUrl: 'https://i.imgur.com/vidXcRm.png', 507 | imgIconUrl: 'https://i.imgur.com/1Jy6K9i.png', 508 | imgInfo: 'from herself ( https://twitter.com/Emerald_ch_ )', 509 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/emerald-pop.mp3', 510 | audioInfo: 'from herself' 511 | } 512 | ] 513 | }) 514 | await WaifuRepo.upsertWaifu({ 515 | urlId: 'pomu', 516 | name: '波姆', 517 | modeConfigList: [ 518 | { 519 | modeName: 'default', 520 | imgNormalUrl: 'https://i.imgur.com/NL71Aig.png', 521 | imgPopUrl: 'https://i.imgur.com/VMLdSR4.jpg', 522 | imgIconUrl: 'https://i.imgur.com/thI1cmp.png', 523 | imgInfo: 'https://twitter.com/Otter_Pomu/status/1416707720430850055 & https://drive.google.com/drive/folders/1egozBgLf-GgnLCjJt20KexV8E7-ow-2u', 524 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/pomu-pop.mp3', 525 | audioInfo: 'https://youtu.be/Ak-jkujxHDc?t=6682' 526 | } 527 | ] 528 | }) 529 | await WaifuRepo.upsertWaifu({ 530 | urlId: 'boureirabbi', 531 | name: '紡霊拉比', 532 | modeConfigList: [ 533 | { 534 | modeName: 'default', 535 | imgNormalUrl: 'https://i.imgur.com/hS5j1nl.png', 536 | imgPopUrl: 'https://i.imgur.com/9znI7VY.png', 537 | imgIconUrl: 'https://i.imgur.com/McKysPx.png', 538 | imgInfo: 'from herself', 539 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/boureirabbi-pop.mp3', 540 | audioInfo: 'https://youtu.be/JdJAU8hTHYc?t=2' 541 | } 542 | ] 543 | }) 544 | await WaifuRepo.upsertWaifu({ 545 | urlId: 'linglan', 546 | name: '森森鈴蘭', 547 | modeConfigList: [ 548 | { 549 | modeName: 'default', 550 | imgNormalUrl: 'https://i.imgur.com/xwGmGln.png', 551 | imgPopUrl: 'https://i.imgur.com/jnWdBqj.png', 552 | imgIconUrl: 'https://i.imgur.com/ay0cp6X.png', 553 | imgInfo: 'https://youtu.be/RkfzhWcSX2I?t=1301', 554 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/linglan-never-up-1.mp3', 555 | audioInfo: 'https://youtu.be/RkfzhWcSX2I?t=1355' 556 | }, 557 | { 558 | modeName: 'up-down', 559 | imgNormalUrl: 'https://i.imgur.com/xwGmGln.png', 560 | imgPopUrl: 'https://i.imgur.com/jnWdBqj.png', 561 | imgIconUrl: 'https://i.imgur.com/ay0cp6X.png', 562 | imgInfo: 'https://youtu.be/RkfzhWcSX2I?t=1301', 563 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/linglan-never-up-1.mp3', 564 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/linglan-never-up-2.mp3', 565 | audioInfo: 'https://youtu.be/RkfzhWcSX2I?t=1355' 566 | }, 567 | { 568 | modeName: 'never-gonna-give', 569 | imgNormalUrl: 'https://i.imgur.com/xwGmGln.png', 570 | imgPopUrl: 'https://i.imgur.com/jnWdBqj.png', 571 | imgIconUrl: 'https://i.imgur.com/ay0cp6X.png', 572 | imgInfo: 'https://youtu.be/RkfzhWcSX2I?t=1301', 573 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/linglan-never-gonna-give-1.mp3', 574 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/linglan-never-gonna-give-2.mp3', 575 | audioInfo: 'https://youtu.be/RkfzhWcSX2I?t=1478' 576 | } 577 | ] 578 | }) 579 | await WaifuRepo.upsertWaifu({ 580 | urlId: 'ukuruniru', 581 | name: '烏庫魯尼魯', 582 | modeConfigList: [ 583 | { 584 | modeName: 'default', 585 | imgNormalUrl: 'https://i.imgur.com/RB6Q5b8.jpg', 586 | imgPopUrl: 'https://i.imgur.com/YHxe0ec.jpg', 587 | imgIconUrl: 'https://i.imgur.com/EUnsA4i.png', 588 | imgInfo: 'https://youtu.be/aXq2HOvJgS8?t=62', 589 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/ukuruniru-pop.mp3', 590 | audioInfo: 'https://youtu.be/aXq2HOvJgS8?t=62' 591 | } 592 | ] 593 | }) 594 | await WaifuRepo.upsertWaifu({ 595 | urlId: 'kazari-lua', 596 | name: '風莉ルア', 597 | modeConfigList: [ 598 | { 599 | modeName: 'default', 600 | imgNormalUrl: 'https://i.imgur.com/niAd6BV.jpg', 601 | imgPopUrl: 'https://i.imgur.com/8jAsS6R.jpg', 602 | imgIconUrl: 'https://i.imgur.com/1xnjioc.png', 603 | imgInfo: 'https://twitter.com/Kazari_Lua/status/1415258802459344909 & https://twitter.com/Kazari_Lua/status/1422515979804758018' 604 | } 605 | ] 606 | }) 607 | await WaifuRepo.upsertWaifu({ 608 | urlId: 'hennnisu', 609 | name: 'Hennnisu', 610 | modeConfigList: [ 611 | { 612 | modeName: 'default', 613 | imgNormalUrl: 'https://i.imgur.com/lWmVLGD.png', 614 | imgPopUrl: 'https://i.imgur.com/qaSCOM8.jpg', 615 | imgIconUrl: 'https://i.imgur.com/qcbFxG3.png', 616 | imgInfo: 'https://youtu.be/gORMDWE9zVQ?t=226', 617 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/hennnisu-pop.mp3', 618 | audioInfo: 'https://youtu.be/gORMDWE9zVQ?t=226' 619 | } 620 | ] 621 | }) 622 | await WaifuRepo.upsertWaifu({ 623 | urlId: 'rushia', 624 | name: '潤羽るしあ', 625 | modeConfigList: [ 626 | { 627 | modeName: 'default', 628 | imgNormalUrl: 'https://i.imgur.com/RiZ2Z7y.jpg', 629 | imgPopUrl: 'https://i.imgur.com/c032cmY.jpg', 630 | imgIconUrl: 'https://i.imgur.com/440PpSX.png', 631 | imgInfo: 'https://twitter.com/uruharushia/status/1375032797623066624', 632 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/rushia-pop.mp3', 633 | audioInfo: 'https://youtu.be/mgEzgLovw8U?t=3943' 634 | } 635 | ] 636 | }) 637 | await WaifuRepo.upsertWaifu({ 638 | urlId: 'elira', 639 | name: 'Elira Pendora', 640 | modeConfigList: [ 641 | { 642 | modeName: 'default', 643 | imgNormalUrl: 'https://i.imgur.com/g7AAg1Z.png', 644 | imgPopUrl: 'https://i.imgur.com/98RqHYy.png', 645 | imgIconUrl: 'https://i.imgur.com/TxMkUw3.png', 646 | imgInfo: 'https://youtu.be/8NDzqxHzKcU', 647 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/elira-pop.mp3', 648 | audioInfo: 'https://youtu.be/SxIYv3er4c0' 649 | } 650 | ] 651 | }) 652 | await WaifuRepo.upsertWaifu({ 653 | urlId: 'arisuaha', 654 | name: '夢姬(ありす)', 655 | modeConfigList: [ 656 | { 657 | modeName: 'default', 658 | imgNormalUrl: 'https://i.imgur.com/bnJLGZ7.png', 659 | imgPopUrl: 'https://i.imgur.com/JCTWcoz.png', 660 | imgIconUrl: 'https://i.imgur.com/kbPHvSa.png', 661 | imgInfo: 'from herself', 662 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/arisuaha-pop.mp3', 663 | audioInfo: 'https://youtu.be/y5VW-76MclY' 664 | } 665 | ] 666 | }) 667 | await WaifuRepo.upsertWaifu({ 668 | urlId: 'tomoe-shirayuki', 669 | name: '白雪巴', 670 | modeConfigList: [ 671 | { 672 | modeName: 'default', 673 | imgNormalUrl: 'https://i.imgur.com/CIk57SG.jpg', 674 | imgPopUrl: 'https://i.imgur.com/lrWsNet.jpeg', 675 | imgIconUrl: 'https://i.imgur.com/iynwQlM.png', 676 | imgInfo: 'from twitter but not sure which tweet ( https://twitter.com/Tomoe_Shirayuki )', 677 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/tomoe-shirayuki-pop-1.mp3', 678 | audioInfo: 'https://youtu.be/pGSfEs3ZDDE?t=1423' 679 | } 680 | ] 681 | }) 682 | await WaifuRepo.upsertWaifu({ 683 | urlId: 'yumeri', 684 | name: '夢理', 685 | modeConfigList: [ 686 | { 687 | modeName: 'default', 688 | imgNormalUrl: 'https://i.imgur.com/yLrN3Yl.png', 689 | imgPopUrl: 'https://i.imgur.com/667cSif.png', 690 | imgIconUrl: 'https://i.imgur.com/rw0fde5.png', 691 | imgInfo: 'from herself', 692 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/yumeri-pop.mp3', 693 | audioInfo: 'from herself' 694 | } 695 | ] 696 | }) 697 | await WaifuRepo.upsertWaifu({ 698 | urlId: 'rumii', 699 | name: '如月ルミィ', 700 | modeConfigList: [ 701 | { 702 | modeName: 'default', 703 | imgNormalUrl: 'https://i.imgur.com/9fKueaa.png', 704 | imgPopUrl: 'https://i.imgur.com/nr1vbfi.png', 705 | imgIconUrl: 'https://i.imgur.com/jhKFU43.png', 706 | imgInfo: 'https://youtu.be/1PooiwI_sK4?t=3228', 707 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/rumii-pop.mp3', 708 | audioInfo: 'https://youtu.be/29Bv0SIUCw0?t=432' 709 | } 710 | ] 711 | }) 712 | await WaifuRepo.upsertWaifu({ 713 | urlId: 'poruko', 714 | name: '黒井夜子', 715 | modeConfigList: [ 716 | { 717 | modeName: 'default', 718 | imgNormalUrl: 'https://i.imgur.com/W90hprs.jpeg', 719 | imgPopUrl: 'https://i.imgur.com/lwIum9B.jpeg', 720 | imgIconUrl: 'https://i.imgur.com/VfVm5SQ.png', 721 | imgInfo: 'from herself', 722 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/poruko-pop.mp3', 723 | audioInfo: 'from herself' 724 | } 725 | ] 726 | }) 727 | await WaifuRepo.upsertWaifu({ 728 | urlId: 'batsu', 729 | name: '荒幽ばつ', 730 | modeConfigList: [ 731 | { 732 | modeName: 'default', 733 | imgNormalUrl: 'https://i.imgur.com/PXrh3CT.png', 734 | imgPopUrl: 'https://i.imgur.com/uVQmDkk.png', 735 | imgIconUrl: 'https://i.imgur.com/8ouXBU8.png', 736 | imgInfo: 'from himself', 737 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/batsu-pop.mp3', 738 | audioInfo: 'from himself' 739 | } 740 | ] 741 | }) 742 | await WaifuRepo.upsertWaifu({ 743 | urlId: 'beimu', 744 | name: '唄姆·拉奇亞', 745 | modeConfigList: [ 746 | { 747 | modeName: 'default', 748 | imgNormalUrl: 'https://i.imgur.com/Rn8UVJQ.png', 749 | imgPopUrl: 'https://i.imgur.com/zX6nRcY.png', 750 | imgIconUrl: 'https://i.imgur.com/Rn8UVJQ.png', 751 | imgInfo: 'https://youtu.be/F-NKP2G2Q1Q & https://youtu.be/9mloZJYj51E' 752 | } 753 | ] 754 | }) 755 | await WaifuRepo.upsertWaifu({ 756 | urlId: 'heanna-sumire', 757 | name: '平安名 すみれ', 758 | modeConfigList: [ 759 | { 760 | modeName: 'default', 761 | imgNormalUrl: 'https://i.imgur.com/gqZIR2l.png', 762 | imgPopUrl: 'https://i.imgur.com/L2bNPYl.jpeg', 763 | imgIconUrl: 'https://i.imgur.com/CfCOHAi.png', 764 | imgInfo: 'https://www.lovelive-anime.jp/yuigaoka/member/', 765 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/heanna-sumire-pop.mp3', 766 | audioInfo: 'https://youtu.be/R2_-weLoIA8' 767 | } 768 | ] 769 | }) 770 | await WaifuRepo.upsertWaifu({ 771 | urlId: 'kaina', 772 | name: '灰名Kaina', 773 | modeConfigList: [ 774 | { 775 | modeName: 'default', 776 | imgNormalUrl: 'https://i.imgur.com/ECPKS1V.jpg', 777 | imgPopUrl: 'https://i.imgur.com/QcoKGwg.jpg', 778 | imgIconUrl: 'https://i.imgur.com/zxRAgzx.png', 779 | imgInfo: 'https://youtu.be/vJTj3ydunlU?t=5', 780 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/kaina-pop.mp3', 781 | audioInfo: 'https://youtu.be/lh_pwXsFLhE' 782 | } 783 | ] 784 | }) 785 | await WaifuRepo.upsertWaifu({ 786 | urlId: 'mya', 787 | name: '米亞Mya', 788 | modeConfigList: [ 789 | { 790 | modeName: 'default', 791 | imgNormalUrl: 'https://i.imgur.com/x3XRtAh.png', 792 | imgPopUrl: 'https://i.imgur.com/rk4EH8I.png', 793 | imgIconUrl: 'https://i.imgur.com/F78vJKs.png', 794 | imgInfo: 'https://youtu.be/DYOwcC9x3Vs', 795 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/mya-pop.mp3', 796 | audioInfo: 'https://youtu.be/JETAq8w-8DA?t=6165' 797 | } 798 | ] 799 | }) 800 | await WaifuRepo.upsertWaifu({ 801 | urlId: 'mafumafu', 802 | name: 'まふまふ', 803 | modeConfigList: [ 804 | { 805 | modeName: 'default', 806 | imgNormalUrl: 'https://i.imgur.com/26oqRS4.jpg', 807 | imgPopUrl: 'https://i.imgur.com/FYNtqsd.png', 808 | imgIconUrl: 'https://i.imgur.com/85Yl9O6.png', 809 | imgInfo: 'https://youtu.be/xrDruN69QCw', 810 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/mafumafu-pop.mp3', 811 | audioInfo: 'https://youtu.be/xrDruN69QCw' 812 | } 813 | ] 814 | }) 815 | await WaifuRepo.upsertWaifu({ 816 | urlId: '04', 817 | name: '零肆04', 818 | modeConfigList: [ 819 | { 820 | modeName: 'default', 821 | imgNormalUrl: 'https://i.imgur.com/JGvio6g.jpeg', 822 | imgPopUrl: 'https://i.imgur.com/aAYd2UM.jpeg', 823 | imgIconUrl: 'https://i.imgur.com/Z49V1VN.png', 824 | imgInfo: 'from her dc server', 825 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/04-ara.mp3', 826 | audioInfo: 'https://www.youtube.com/channel/UCNZyvI_TjyJyBVtdHEsO7HA' 827 | } 828 | ] 829 | }) 830 | await WaifuRepo.upsertWaifu({ 831 | urlId: 'kurita', 832 | name: '鼠屋栗太', 833 | modeConfigList: [ 834 | { 835 | modeName: 'default', 836 | imgNormalUrl: 'https://i.imgur.com/c640G1h.png', 837 | imgPopUrl: 'https://i.imgur.com/jD3mDiu.png', 838 | imgIconUrl: 'https://i.imgur.com/c640G1h.png', 839 | imgInfo: 'https://youtu.be/HXvQlvn2yIk' 840 | } 841 | ] 842 | }) 843 | await WaifuRepo.upsertWaifu({ 844 | urlId: 'akina', 845 | name: '三枝明那', 846 | modeConfigList: [ 847 | { 848 | modeName: 'default', 849 | imgNormalUrl: 'https://i.imgur.com/UwnbrCt.png', 850 | imgPopUrl: 'https://i.imgur.com/VyHgNeA.png', 851 | imgIconUrl: 'https://i.imgur.com/64TFeW5.png', 852 | imgInfo: 'https://twitter.com/333akina/status/1318927020126990336', 853 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/akina-pop.mp3', 854 | audioInfo: 'https://youtu.be/QHUnceexHCE' 855 | } 856 | ] 857 | }) 858 | await WaifuRepo.upsertWaifu({ 859 | urlId: 'sukoya', 860 | name: '健屋花那', 861 | modeConfigList: [ 862 | { 863 | modeName: 'default', 864 | imgNormalUrl: 'https://i.imgur.com/LzS580a.jpeg', 865 | imgPopUrl: 'https://i.imgur.com/e0Snp2u.jpeg', 866 | imgIconUrl: 'https://i.imgur.com/WPRY3W8.png', 867 | imgInfo: 'https://youtu.be/APCKILJjG-o 8:59 & 9:20', 868 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/sukoya-pop.mp3', 869 | audioInfo: 'from stream' 870 | } 871 | ] 872 | }) 873 | await WaifuRepo.upsertWaifu({ 874 | urlId: 'speedwagon', 875 | name: 'Robert E. O. Speedwagon', 876 | modeConfigList: [ 877 | { 878 | modeName: 'default', 879 | imgNormalUrl: 'https://i.imgur.com/b6nr5Kg.jpeg', 880 | imgPopUrl: 'https://i.imgur.com/xfAWnX4.jpeg', 881 | imgIconUrl: 'https://i.imgur.com/xfAWnX4.jpeg', 882 | imgInfo: 'JOJO 1' 883 | } 884 | ] 885 | }) 886 | await WaifuRepo.upsertWaifu({ 887 | urlId: 'akito', 888 | name: '緋佐あきと', 889 | modeConfigList: [ 890 | { 891 | modeName: 'default', 892 | imgNormalUrl: 'https://i.imgur.com/DLX6TF2.png', 893 | imgPopUrl: 'https://i.imgur.com/AwYMf6U.png', 894 | imgIconUrl: 'https://i.imgur.com/i2j5rhu.png', 895 | imgInfo: '', 896 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/akito-pop.mp3', 897 | audioInfo: 'https://youtu.be/vGz-FpQE5Eo' 898 | } 899 | ] 900 | }) 901 | await WaifuRepo.upsertWaifu({ 902 | urlId: 'hkmkmui', 903 | name: 'MK妹', 904 | modeConfigList: [ 905 | { 906 | modeName: 'default', 907 | imgNormalUrl: 'https://i.imgur.com/yVz3gok.png', 908 | imgPopUrl: 'https://i.imgur.com/cAggpkt.png', 909 | imgIconUrl: 'https://i.imgur.com/uonmjO3.png', 910 | imgInfo: 'https://www.youtube.com/channel/UCO62chyehk6pX7OitrnJAUg/', 911 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/hkmkmui-pop.mp3', 912 | audioInfo: 'https://youtu.be/iNWmaKPzgII?t=71' 913 | } 914 | ] 915 | }) 916 | await WaifuRepo.upsertWaifu({ 917 | urlId: 'ibrahim', 918 | name: 'イブラヒム', 919 | modeConfigList: [ 920 | { 921 | modeName: 'default', 922 | imgNormalUrl: 'https://i.imgur.com/PH5gOfL.jpeg', 923 | imgPopUrl: 'https://i.imgur.com/DtavimR.jpeg', 924 | imgIconUrl: 'https://i.imgur.com/PH5gOfL.jpeg', 925 | imgInfo: 'https://youtu.be/IsmFpgGWGKg', 926 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/ibrahim-pop.mp3', 927 | audioInfo: 'https://youtu.be/SQiS_N1ZDjI' 928 | } 929 | ] 930 | }) 931 | await WaifuRepo.upsertWaifu({ 932 | urlId: 'haruka', 933 | name: '星見遙', 934 | modeConfigList: [ 935 | { 936 | modeName: 'default', 937 | imgNormalUrl: 'https://i.imgur.com/oyyO7au.jpeg', 938 | imgPopUrl: 'https://i.imgur.com/5xRfTIj.jpeg', 939 | imgIconUrl: 'https://i.imgur.com/ml43yZw.png', 940 | imgInfo: 'https://youtu.be/P8cbbfidqM4?t=14709', 941 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/haruka-pop.mp3', 942 | audioInfo: 'https://youtu.be/d1O21n5SZmE' 943 | } 944 | ] 945 | }) 946 | await WaifuRepo.upsertWaifu({ 947 | urlId: 'venti', 948 | name: '溫迪', 949 | modeConfigList: [ 950 | { 951 | modeName: 'default', 952 | imgNormalUrl: 'https://i.imgur.com/rvkaR7p.jpeg', 953 | imgPopUrl: 'https://i.imgur.com/3HzPBTM.jpeg', 954 | imgIconUrl: 'https://i.imgur.com/bh0mIlh.png', 955 | imgInfo: 'from Genshin Impact screenshot', 956 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/venti-pop.mp3', 957 | audioInfo: 'https://youtu.be/nz6iiDsyZGM' 958 | } 959 | ] 960 | }) 961 | await WaifuRepo.upsertWaifu({ 962 | urlId: 'kusaka-ice', 963 | name: '日下 氷 KusakaICE', 964 | modeConfigList: [ 965 | { 966 | modeName: 'default', 967 | imgNormalUrl: 'https://i.imgur.com/0tx8Y2t.png', 968 | imgPopUrl: 'https://i.imgur.com/Y8M3gMx.png', 969 | imgIconUrl: 'https://i.imgur.com/UyOXnhz.png', 970 | imgInfo: 'https://youtu.be/cDRqfwbmiRo', 971 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/kusaka-ice-pop.mp3', 972 | audioInfo: 'https://youtu.be/sDA1GBdCmSs' 973 | } 974 | ] 975 | }) 976 | await WaifuRepo.upsertWaifu({ 977 | urlId: 'kai-mayuzumi', 978 | name: '黛灰', 979 | modeConfigList: [ 980 | { 981 | modeName: 'default', 982 | imgNormalUrl: 'https://i.imgur.com/EoUVnz2.png', 983 | imgPopUrl: 'https://i.imgur.com/0CJIza0.png', 984 | imgIconUrl: 'https://i.imgur.com/6S52lw8.png', 985 | imgInfo: 'https://youtu.be/EJhbV2CAiM4' 986 | } 987 | ] 988 | }) 989 | await WaifuRepo.upsertWaifu({ 990 | urlId: 'fengxu', 991 | name: '風絮', 992 | modeConfigList: [ 993 | { 994 | modeName: 'default', 995 | imgNormalUrl: 'https://i.imgur.com/VKIKzBm.png', 996 | imgPopUrl: 'https://i.imgur.com/4WqWY2W.png', 997 | imgIconUrl: 'https://i.imgur.com/ybHjD4M.png', 998 | imgInfo: 'https://youtu.be/rwt1o8OPBT0 24:24 & 24:28', 999 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/fengxu-pop.mp3', 1000 | audioInfo: 'from his discord server' 1001 | } 1002 | ] 1003 | }) 1004 | await WaifuRepo.upsertWaifu({ 1005 | urlId: 'veibae', 1006 | name: 'Veibae', 1007 | modeConfigList: [ 1008 | { 1009 | modeName: 'default', 1010 | imgNormalUrl: 'https://i.imgur.com/ZxWNJcR.png', 1011 | imgPopUrl: 'https://i.imgur.com/Zd1e0ew.png', 1012 | imgIconUrl: 'https://i.imgur.com/3pEof1O.png', 1013 | imgInfo: 'https://youtu.be/QAuu7FuKCec?t=169', 1014 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/veibae-pop.mp3', 1015 | audioInfo: 'https://youtu.be/QAuu7FuKCec?t=170' 1016 | } 1017 | ] 1018 | }) 1019 | await WaifuRepo.upsertWaifu({ 1020 | urlId: 'ina', 1021 | name: 'Ninomae Ina\'nis', 1022 | modeConfigList: [ 1023 | { 1024 | modeName: 'default', 1025 | imgNormalUrl: 'https://i.imgur.com/oVcJL6F.jpeg', 1026 | imgPopUrl: 'https://i.imgur.com/wQrOpnU.jpeg', 1027 | imgIconUrl: 'https://i.imgur.com/48YijDf.png', 1028 | imgInfo: 'https://youtu.be/tAvOULiZuHQ?t=12', 1029 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/ina-wah.mp3', 1030 | audioInfo: 'https://youtu.be/rqSxHrplZ34?t=174' 1031 | }, 1032 | { 1033 | modeName: '2', 1034 | imgNormalUrl: 'https://i.imgur.com/oVcJL6F.jpeg', 1035 | imgPopUrl: 'https://i.imgur.com/wQrOpnU.jpeg', 1036 | imgIconUrl: 'https://i.imgur.com/48YijDf.png', 1037 | imgInfo: 'https://youtu.be/tAvOULiZuHQ?t=12', 1038 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/ina-1.mp3', 1039 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/ina-2.mp3', 1040 | audioInfo: 'https://youtu.be/tAvOULiZuHQ' 1041 | } 1042 | ] 1043 | }) 1044 | await WaifuRepo.upsertWaifu({ 1045 | urlId: 'hyakuya-shirotori', 1046 | name: '百夜白鳥', 1047 | modeConfigList: [ 1048 | { 1049 | modeName: 'default', 1050 | imgNormalUrl: 'https://i.imgur.com/sjojGOa.png', 1051 | imgPopUrl: 'https://i.imgur.com/sFbCioR.png', 1052 | imgIconUrl: 'https://i.imgur.com/icF9DZg.png', 1053 | imgInfo: 'https://youtu.be/HI2tkTk0hYI', 1054 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/hyakuya-shirotori-pop.mp3', 1055 | audioInfo: 'https://youtu.be/HI2tkTk0hYI?t=8941' 1056 | } 1057 | ] 1058 | }) 1059 | await WaifuRepo.upsertWaifu({ 1060 | urlId: 'inori', 1061 | name: '秋月イノリ', 1062 | modeConfigList: [ 1063 | { 1064 | modeName: 'default', 1065 | imgNormalUrl: 'https://i.imgur.com/zRvoxhy.png', 1066 | imgPopUrl: 'https://i.imgur.com/drkYYgt.png', 1067 | imgIconUrl: 'https://i.imgur.com/Ayx86kB.png', 1068 | imgInfo: '', 1069 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/inori-pop.mp3', 1070 | audioInfo: 'https://youtu.be/rnNWiZAUsws' 1071 | } 1072 | ] 1073 | }) 1074 | await WaifuRepo.upsertWaifu({ 1075 | urlId: 'polka', 1076 | name: '尾丸ポルカ', 1077 | modeConfigList: [ 1078 | { 1079 | modeName: 'default', 1080 | imgNormalUrl: 'https://i.imgur.com/IZGgEkT.png', 1081 | imgPopUrl: 'https://i.imgur.com/Pq1eF9w.png', 1082 | imgIconUrl: 'https://i.imgur.com/xd1I4b8.png', 1083 | imgInfo: 'https://youtu.be/Fgm_f6pO0uM?t=10', 1084 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/polka-pop.mp3', 1085 | audioInfo: 'https://youtu.be/Fgm_f6pO0uM' 1086 | } 1087 | ] 1088 | }) 1089 | await WaifuRepo.upsertWaifu({ 1090 | urlId: 'dollaeggtartv', 1091 | name: 'Dolla朵拉', 1092 | modeConfigList: [ 1093 | { 1094 | modeName: 'default', 1095 | imgNormalUrl: 'https://i.imgur.com/kdVYw9n.png', 1096 | imgPopUrl: 'https://i.imgur.com/VD53ZYR.png', 1097 | imgIconUrl: 'https://i.imgur.com/u9UhbkA.png', 1098 | imgInfo: 'https://youtu.be/wZfUCflpf4E', 1099 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/dollaeggtartv-pop.mp3', 1100 | audioInfo: 'https://youtu.be/wZfUCflpf4E' 1101 | } 1102 | ] 1103 | }) 1104 | await WaifuRepo.upsertWaifu({ 1105 | urlId: 'jhu-dao', 1106 | name: '朱道', 1107 | modeConfigList: [ 1108 | { 1109 | modeName: 'default', 1110 | imgNormalUrl: 'https://i.imgur.com/j9z7tZ8.jpeg', 1111 | imgPopUrl: 'https://i.imgur.com/LmeFkRc.jpeg', 1112 | imgIconUrl: 'https://i.imgur.com/OPRVmOC.png', 1113 | imgInfo: 'https://youtu.be/y-nP-BGBRaQ?t=2778', 1114 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/jhu-dao-pop.mp3', 1115 | audioInfo: 'https://youtu.be/-2_lnEewp7U?t=9367' 1116 | } 1117 | ] 1118 | }) 1119 | await WaifuRepo.upsertWaifu({ 1120 | urlId: 'katosparrow', 1121 | name: '加藤小麻雀', 1122 | modeConfigList: [ 1123 | { 1124 | modeName: 'default', 1125 | imgNormalUrl: 'https://i.imgur.com/cba60DG.png', 1126 | imgPopUrl: 'https://i.imgur.com/0R3Hrmt.png', 1127 | imgIconUrl: 'https://i.imgur.com/cba60DG.png', 1128 | imgInfo: 'https://youtu.be/gHompc-T0o8?t=1996', 1129 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/katosparrow-pop.mp3', 1130 | audioInfo: 'https://youtu.be/1KPefG31JEs?t=1352' 1131 | } 1132 | ] 1133 | }) 1134 | await WaifuRepo.upsertWaifu({ 1135 | urlId: 'belmond-banderas', 1136 | name: 'ベルモンド・バンデラス', 1137 | modeConfigList: [ 1138 | { 1139 | modeName: 'default', 1140 | imgNormalUrl: 'https://i.imgur.com/PiZFIHQ.jpeg', 1141 | imgPopUrl: 'https://i.imgur.com/jODoT7u.jpeg', 1142 | imgIconUrl: 'https://i.imgur.com/ngJEfGx.png', 1143 | imgInfo: 'https://youtu.be/25h0GVgYCPs?t=4132', 1144 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/belmond-banderas-a.mp3', 1145 | audioInfo: 'https://youtu.be/JOBCw4E6tP0' 1146 | } 1147 | ] 1148 | }) 1149 | await WaifuRepo.upsertWaifu({ 1150 | urlId: 'calli', 1151 | name: 'Mori Calliope', 1152 | modeConfigList: [ 1153 | { 1154 | modeName: 'default', 1155 | imgNormalUrl: 'https://i.imgur.com/WhTx9ct.jpeg', 1156 | imgPopUrl: 'https://i.imgur.com/4VqiJar.jpeg', 1157 | imgIconUrl: 'https://i.imgur.com/4VqiJar.jpeg', 1158 | imgInfo: 'https://knowyourmeme.com/memes/people/mori-calliope/ & https://soundcloud.com/mtell/calliope-mori-rip-mtell-remix', 1159 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/calli-pop.mp3', 1160 | audioInfo: 'https://youtu.be/zxVgPS_sMjM' 1161 | } 1162 | ] 1163 | }) 1164 | await WaifuRepo.upsertWaifu({ 1165 | urlId: 'texas', 1166 | name: '德克薩斯', 1167 | modeConfigList: [ 1168 | { 1169 | modeName: 'default', 1170 | imgNormalUrl: 'https://i.imgur.com/3Gm1rCP.jpeg', 1171 | imgPopUrl: 'https://i.imgur.com/66SkW00.jpeg', 1172 | imgIconUrl: 'https://i.imgur.com/wDggRPJ.png', 1173 | imgInfo: 'Arknights official picture', 1174 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/texas-pop.mp3', 1175 | audioInfo: 'from Arknights game sound' 1176 | } 1177 | ] 1178 | }) 1179 | await WaifuRepo.upsertWaifu({ 1180 | urlId: 'mochitsugi-luna', 1181 | name: '望月ルーナ', 1182 | modeConfigList: [ 1183 | { 1184 | modeName: 'default', 1185 | imgNormalUrl: 'https://i.imgur.com/48FUnll.png', 1186 | imgPopUrl: 'https://i.imgur.com/eY2GOLD.png', 1187 | imgIconUrl: 'https://i.imgur.com/GXjAEhF.png', 1188 | imgInfo: 'from vt ( https://twitter.com/luna_mochitsugi )', 1189 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/mochitsugi-luna-pop.mp3', 1190 | audioInfo: 'https://youtu.be/wOhgSsoy-y0?t=4570' 1191 | }, 1192 | { 1193 | modeName: 'cat', 1194 | imgNormalUrl: 'https://i.imgur.com/LbgJOnC.png', 1195 | imgPopUrl: 'https://i.imgur.com/5Visr31.png', 1196 | imgIconUrl: 'https://i.imgur.com/8byXuRY.png', 1197 | imgInfo: 'from vt ( https://twitter.com/luna_mochitsugi )', 1198 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/mochitsugi-luna-pop.mp3', 1199 | audioInfo: 'https://youtu.be/wOhgSsoy-y0?t=4570' 1200 | } 1201 | ] 1202 | }) 1203 | await WaifuRepo.upsertWaifu({ 1204 | urlId: 'enn', 1205 | name: 'Enn Sings', 1206 | modeConfigList: [ 1207 | { 1208 | modeName: 'default', 1209 | imgNormalUrl: 'https://i.imgur.com/NCiK3Q2.png', 1210 | imgPopUrl: 'https://i.imgur.com/lL7vZbg.png', 1211 | imgIconUrl: 'https://i.imgur.com/NCiK3Q2.png', 1212 | imgInfo: 'https://youtu.be/7txXTRX2oe8?t=3', 1213 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/enn-1.mp3', 1214 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/enn-2.mp3', 1215 | audioInfo: 'https://youtu.be/ShXRVPLFdxo?t=2525 & https://youtu.be/h1Dr4B3rBII?t=2683' 1216 | } 1217 | ] 1218 | }) 1219 | await WaifuRepo.upsertWaifu({ 1220 | urlId: 'kagamihayato', 1221 | name: '加賀美 ハヤト', 1222 | modeConfigList: [ 1223 | { 1224 | modeName: 'default', 1225 | imgNormalUrl: 'https://i.imgur.com/YgeXyUZ.png', 1226 | imgPopUrl: 'https://i.imgur.com/SvzuxPA.png', 1227 | imgIconUrl: 'https://i.imgur.com/YgeXyUZ.png', 1228 | imgInfo: 'https://zh.moegirl.org.cn/%E5%8A%A0%E8%B4%BA%E7%BE%8E%E9%9A%BC%E4%BA%BA', 1229 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/kagamihayato-pop.mp3', 1230 | audioInfo: 'https://youtu.be/4-jpsUKO50I?t=50' 1231 | } 1232 | ] 1233 | }) 1234 | await WaifuRepo.upsertWaifu({ 1235 | urlId: 'shien', 1236 | name: '影山シエン', 1237 | modeConfigList: [ 1238 | { 1239 | modeName: 'default', 1240 | imgNormalUrl: 'https://i.imgur.com/wmhlzK3.png', 1241 | imgPopUrl: 'https://i.imgur.com/ZfDIW7Q.png', 1242 | imgIconUrl: 'https://i.imgur.com/dpwLvVm.png', 1243 | imgInfo: 'https://youtu.be/fLC438LIDwE 18:35 & 19:37', 1244 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/shien-pop.mp3', 1245 | audioInfo: 'https://youtu.be/sA6o0aTnn1w' 1246 | } 1247 | ] 1248 | }) 1249 | await WaifuRepo.upsertWaifu({ 1250 | urlId: 'noel', 1251 | name: '白銀ノエル', 1252 | modeConfigList: [ 1253 | { 1254 | modeName: 'default', 1255 | imgNormalUrl: 'https://i.imgur.com/5XRGCn8.jpg', 1256 | imgPopUrl: 'https://i.imgur.com/wYmnRYS.jpg', 1257 | imgIconUrl: 'https://i.imgur.com/Q7G5BKK.png', 1258 | imgInfo: 'https://twitter.com/illustr_speaker/status/1434835933828829188 & https://twitter.com/shiroganenoel/status/1437024594896965639', 1259 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/noel-a.mp3', 1260 | audioInfo: 'https://youtu.be/RE73Alg-2WQ' 1261 | } 1262 | ] 1263 | }) 1264 | await WaifuRepo.upsertWaifu({ 1265 | urlId: 'yuma-tsukumo', 1266 | name: '九十九遊馬', 1267 | modeConfigList: [ 1268 | { 1269 | modeName: 'default', 1270 | imgNormalUrl: 'https://i.imgur.com/8ekGM2G.jpg', 1271 | imgPopUrl: 'https://i.imgur.com/xOojm31.png', 1272 | imgIconUrl: 'https://i.imgur.com/JxUbOI4.png', 1273 | imgInfo: 'https://twitter.com/KonamiUK/status/1310862999968776 & https://yugioh.fandom.com/wiki/ZEXAL_(Duel_Links)', 1274 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/yuma-tsukumo-pop.mp3', 1275 | audioInfo: 'https://youtu.be/R5MrpJ3NN8c' 1276 | } 1277 | ] 1278 | }) 1279 | await WaifuRepo.upsertWaifu({ 1280 | urlId: '4virtual-tedobear', 1281 | name: '4Virtual TedoBear 泰多貝亞', 1282 | modeConfigList: [ 1283 | { 1284 | modeName: 'default', 1285 | imgNormalUrl: 'https://i.imgur.com/I9ekJWq.png', 1286 | imgPopUrl: 'https://i.imgur.com/dj2pfZh.png', 1287 | imgIconUrl: 'https://i.imgur.com/dj2pfZh.png', 1288 | imgInfo: 'https://www.youtube.com/c/4VirtualTedoBear%E6%B3%B0%E5%A4%9A%E8%B2%9D%E4%BA%9E', 1289 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/4virtual-tedobear-pop.mp3', 1290 | audioInfo: 'from his discord server' 1291 | } 1292 | ] 1293 | }) 1294 | await WaifuRepo.upsertWaifu({ 1295 | urlId: 'otoki', 1296 | name: '音軌オトキ', 1297 | modeConfigList: [ 1298 | { 1299 | modeName: 'default', 1300 | imgNormalUrl: 'https://i.imgur.com/PZMinL0.png', 1301 | imgPopUrl: 'https://i.imgur.com/6AvHjKu.png', 1302 | imgIconUrl: 'https://i.imgur.com/HvjiPAC.png', 1303 | imgInfo: 'https://youtu.be/91EH1QqQGOo', 1304 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/otoki-pop-1.mp3', 1305 | audioInfo: 'https://youtu.be/Gs4fLsT7Sa4' 1306 | } 1307 | ] 1308 | }) 1309 | await WaifuRepo.upsertWaifu({ 1310 | urlId: 'gaku-fushimi', 1311 | name: '伏見ガク', 1312 | modeConfigList: [ 1313 | { 1314 | modeName: 'default', 1315 | imgNormalUrl: 'https://i.imgur.com/skOkNdc.png', 1316 | imgPopUrl: 'https://i.imgur.com/Yqf0lgY.png', 1317 | imgIconUrl: 'https://i.imgur.com/fmZGCIK.png', 1318 | imgInfo: 'https://www.nijisanji.jp/members/gaku-fushimi & https://twitter.com/gaku_fushimi/status/1048229997553496064', 1319 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/gaku-fushimi-pop.mp3', 1320 | audioInfo: 'https://youtu.be/dXEotJJGb5I' 1321 | }, 1322 | { 1323 | modeName: '2', 1324 | imgNormalUrl: 'https://i.imgur.com/ljuO1ak.jpeg', 1325 | imgPopUrl: 'https://i.imgur.com/CPofd9d.jpeg', 1326 | imgIconUrl: 'https://i.imgur.com/aK9t9pW.png', 1327 | imgInfo: 'https://youtu.be/f_T8M75ACZ8 38:08 & 55:10', 1328 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/gaku-fushimi-2-pop.mp3', 1329 | audioInfo: 'https://youtu.be/FfWSRPfs16s' 1330 | } 1331 | ] 1332 | }) 1333 | await WaifuRepo.upsertWaifu({ 1334 | urlId: 'nene', 1335 | name: '桃鈴ねね', 1336 | modeConfigList: [ 1337 | { 1338 | modeName: 'default', 1339 | imgNormalUrl: 'https://i.imgur.com/KKECSds.png', 1340 | imgPopUrl: 'https://i.imgur.com/ubcdrpZ.png', 1341 | imgIconUrl: 'https://i.imgur.com/IpfR7CO.png', 1342 | imgInfo: 'https://youtu.be/oSrcIh-_4VE?t=524 & https://youtu.be/oSrcIh-_4VE?t=462', 1343 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/nene-seal-ow1.mp3', 1344 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/nene-seal-slap.mp3', 1345 | audioInfo: 'https://youtu.be/_BqDWHzripE?t=10724' 1346 | }, 1347 | { 1348 | modeName: '2', 1349 | imgNormalUrl: 'https://i.imgur.com/KKECSds.png', 1350 | imgPopUrl: 'https://i.imgur.com/ubcdrpZ.png', 1351 | imgIconUrl: 'https://i.imgur.com/IpfR7CO.png', 1352 | imgInfo: 'https://youtu.be/oSrcIh-_4VE?t=524 & https://youtu.be/oSrcIh-_4VE?t=462', 1353 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/nene-seal-ow2.mp3', 1354 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/nene-seal-slap.mp3', 1355 | audioInfo: 'https://youtu.be/jQMdS0Jlql4?t=9671' 1356 | } 1357 | ] 1358 | }) 1359 | await WaifuRepo.upsertWaifu({ 1360 | urlId: 'pekora', 1361 | name: '兎田ぺこら', 1362 | modeConfigList: [ 1363 | { 1364 | modeName: 'default', 1365 | imgNormalUrl: 'https://i.imgur.com/LE0MF2t.png', 1366 | imgPopUrl: 'https://i.imgur.com/g26gl5p.png', 1367 | imgIconUrl: 'https://i.imgur.com/IYYOtV1.png', 1368 | imgInfo: 'https://youtu.be/D9mQf56453I?t=1043 & https://youtu.be/D9mQf56453I?t=1067', 1369 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/pekora-peko.mp3', 1370 | audioInfo: 'https://youtu.be/ZCjV0CL7evQ?t=10033' 1371 | }, 1372 | { 1373 | modeName: 'domo', 1374 | imgNormalUrl: 'https://i.imgur.com/5KeAAB6.png', 1375 | imgPopUrl: 'https://i.imgur.com/Wr5xs1t.png', 1376 | imgIconUrl: 'https://i.imgur.com/my2tS86.png', 1377 | imgInfo: 'https://youtu.be/dNZ5mCX2Eds?t=283 & https://youtu.be/dNZ5mCX2Eds?t=9046', 1378 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/pekora-domo-amo.mp3', 1379 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/pekora-domo-do.mp3', 1380 | audioInfo: 'https://youtu.be/NZpCdpjuAB0?t=120' 1381 | } 1382 | ] 1383 | }) 1384 | await WaifuRepo.upsertWaifu({ 1385 | urlId: 'woof-woffle', 1386 | name: '嗚夫沃夫', 1387 | modeConfigList: [ 1388 | { 1389 | modeName: 'default', 1390 | imgNormalUrl: 'https://i.imgur.com/W0nKrsD.png', 1391 | imgPopUrl: 'https://i.imgur.com/7AP03y9.png', 1392 | imgIconUrl: 'https://i.imgur.com/WJC0aof.png', 1393 | imgInfo: 'https://youtu.be/Q3PQFGXMTIM?t=1429', 1394 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/woof-woffle-pop.mp3', 1395 | audioInfo: 'unknow' 1396 | } 1397 | ] 1398 | }) 1399 | await WaifuRepo.upsertWaifu({ 1400 | urlId: 'mikotosatsuki5', 1401 | name: '水琴五月', 1402 | modeConfigList: [ 1403 | { 1404 | modeName: 'default', 1405 | imgNormalUrl: 'https://i.imgur.com/cIox86r.jpg', 1406 | imgPopUrl: 'https://i.imgur.com/HVCbvhf.jpg', 1407 | imgIconUrl: 'https://i.imgur.com/UPS9x9W.png', 1408 | imgInfo: 'https://youtu.be/In163jajImU', 1409 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/mikotosatsuki5-pop-1.mp3', 1410 | audioInfo: 'https://youtu.be/In163jajImU' 1411 | } 1412 | ] 1413 | }) 1414 | await WaifuRepo.upsertWaifu({ 1415 | urlId: 'temma', 1416 | name: '岸堂天真', 1417 | modeConfigList: [ 1418 | { 1419 | modeName: 'default', 1420 | imgNormalUrl: 'https://i.imgur.com/rc29QWY.jpeg', 1421 | imgPopUrl: 'https://i.imgur.com/ktkhhro.jpeg', 1422 | imgIconUrl: 'https://i.imgur.com/s7hnCM8.png', 1423 | imgInfo: 'https://youtu.be/BXZExVa8mWw?t=2053', 1424 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/temma-pop.mp3', 1425 | audioInfo: 'https://youtu.be/BXZExVa8mWw?t=2053' 1426 | } 1427 | ] 1428 | }) 1429 | await WaifuRepo.upsertWaifu({ 1430 | urlId: 'kakeru-yumeoi', 1431 | name: '夢追翔', 1432 | modeConfigList: [ 1433 | { 1434 | modeName: 'default', 1435 | imgNormalUrl: 'https://i.imgur.com/5iiAXrA.png', 1436 | imgPopUrl: 'https://i.imgur.com/8UhFqoM.png', 1437 | imgIconUrl: 'https://i.imgur.com/dmrMVG2.png', 1438 | imgInfo: 'https://youtu.be/aa8uvG2mENg?t=2865', 1439 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/kakeru-yumeoi-pop.mp3', 1440 | audioInfo: 'https://youtu.be/Y5BH8by8P0Y' 1441 | } 1442 | ] 1443 | }) 1444 | await WaifuRepo.upsertWaifu({ 1445 | urlId: 'adano', 1446 | name: '雅達諾', 1447 | modeConfigList: [ 1448 | { 1449 | modeName: 'default', 1450 | imgNormalUrl: 'https://i.imgur.com/FCpDkUc.png', 1451 | imgPopUrl: 'https://i.imgur.com/n1HAbd8.png', 1452 | imgIconUrl: 'https://i.imgur.com/FCpDkUc.png', 1453 | imgInfo: 'from himself ( https://twitter.com/Adano1124 )', 1454 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/adano-pop.mp3', 1455 | audioInfo: 'from himself' 1456 | } 1457 | ] 1458 | }) 1459 | await WaifuRepo.upsertWaifu({ 1460 | urlId: 'miyabi', 1461 | name: '花咲みやび', 1462 | modeConfigList: [ 1463 | { 1464 | modeName: 'default', 1465 | imgNormalUrl: 'https://i.imgur.com/r2Z5lC0.png', 1466 | imgPopUrl: 'https://i.imgur.com/6IhKWE3.jpg', 1467 | imgIconUrl: 'https://i.imgur.com/r2Z5lC0.png', 1468 | imgInfo: 'https://youtu.be/wpZ7WKdKzYs?t=5753 & https://twitter.com/miyabihanasaki/status/1352270151794585603', 1469 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/miyabi-pop.mp3', 1470 | audioInfo: 'https://youtu.be/q6TctPzm0gc?t=2690' 1471 | } 1472 | ] 1473 | }) 1474 | await WaifuRepo.upsertWaifu({ 1475 | urlId: 'cheukcat', 1476 | name: '綽貓喵', 1477 | modeConfigList: [ 1478 | { 1479 | modeName: 'default', 1480 | imgNormalUrl: 'https://i.imgur.com/DYI8BrY.png', 1481 | imgPopUrl: 'https://i.imgur.com/vMRTHsW.png', 1482 | imgIconUrl: 'https://i.imgur.com/6HBSnD6.png', 1483 | imgInfo: 'https://youtu.be/K1IoGZmA3aw?t=2950', 1484 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/cheukcat1.mp3', 1485 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/cheukcat2.mp3', 1486 | audioInfo: 'https://youtu.be/K1IoGZmA3aw?t=2950' 1487 | } 1488 | ] 1489 | }) 1490 | await WaifuRepo.upsertWaifu({ 1491 | urlId: 'astel', 1492 | name: 'アステル・レダ', 1493 | modeConfigList: [ 1494 | { 1495 | modeName: 'default', 1496 | imgNormalUrl: 'https://i.imgur.com/DpTCLTT.jpg', 1497 | imgPopUrl: 'https://i.imgur.com/qgfiK2s.jpg', 1498 | imgIconUrl: 'https://i.imgur.com/tZc2L2l.png', 1499 | imgInfo: 'https://youtu.be/n0lGTyvTE8w?t=493 & https://youtu.be/n0lGTyvTE8w?t=714', 1500 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/astel-pop.mp3', 1501 | audioInfo: 'https://youtu.be/D1g3BNbbDsw?t=1970' 1502 | } 1503 | ] 1504 | }) 1505 | await WaifuRepo.upsertWaifu({ 1506 | urlId: 'canis', 1507 | name: '阿狗Canis', 1508 | modeConfigList: [ 1509 | { 1510 | modeName: 'default', 1511 | imgNormalUrl: 'https://i.imgur.com/RY8uO0R.jpg', 1512 | imgPopUrl: 'https://i.imgur.com/R1n5vEN.jpg', 1513 | imgIconUrl: 'https://i.imgur.com/RY8uO0R.jpg', 1514 | imgInfo: 'https://youtu.be/q9KuTDMfMow?t=4857', 1515 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/canis-pop.mp3', 1516 | audioInfo: 'https://youtu.be/WsngdvtmngE' 1517 | } 1518 | ] 1519 | }) 1520 | await WaifuRepo.upsertWaifu({ 1521 | urlId: 'yumesakimia', 1522 | name: '夢咲ミア', 1523 | modeConfigList: [ 1524 | { 1525 | modeName: 'default', 1526 | imgNormalUrl: 'https://i.imgur.com/S80qEJN.png', 1527 | imgPopUrl: 'https://i.imgur.com/pHR73Hh.png', 1528 | imgIconUrl: 'https://i.imgur.com/S80qEJN.png', 1529 | imgInfo: 'https://youtu.be/5pHBtyO4xNU 0:13 & 0:03', 1530 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/yumesakimia-pop.mp3', 1531 | audioInfo: 'https://youtu.be/5pHBtyO4xNU' 1532 | } 1533 | ] 1534 | }) 1535 | await WaifuRepo.upsertWaifu({ 1536 | urlId: 'oliver-evans', 1537 | name: 'オリバー・エバンス', 1538 | modeConfigList: [ 1539 | { 1540 | modeName: 'default', 1541 | imgNormalUrl: 'https://i.imgur.com/2A1aZ92.jpg', 1542 | imgPopUrl: 'https://i.imgur.com/bhvvmG3.jpg', 1543 | imgIconUrl: 'https://i.imgur.com/p1AmjtU.png', 1544 | imgInfo: 'https://youtu.be/xUJJF6fbSR8?t=1349' 1545 | } 1546 | ] 1547 | }) 1548 | await WaifuRepo.upsertWaifu({ 1549 | urlId: 'suisei', 1550 | name: '星街すいせい', 1551 | modeConfigList: [ 1552 | { 1553 | modeName: 'default', 1554 | imgNormalUrl: 'https://i.imgur.com/AG0J0Tp.png', 1555 | imgPopUrl: 'https://i.imgur.com/TPluqIa.png', 1556 | imgIconUrl: 'https://i.imgur.com/JzWtwku.png', 1557 | imgInfo: 'https://youtu.be/hq-AsszEZIo', 1558 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/suisei-pop.mp3', 1559 | audioInfo: 'https://youtu.be/hq-AsszEZIo' 1560 | }, 1561 | { 1562 | modeName: 'kyomo', 1563 | imgNormalUrl: 'https://i.imgur.com/ToPppcW.png', 1564 | imgPopUrl: 'https://i.imgur.com/An7qbno.png', 1565 | imgIconUrl: 'https://i.imgur.com/6weT0Ex.png', 1566 | imgInfo: 'https://youtu.be/HZmPB0f3cbI?t=53', 1567 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/suisei-kyomo.mp3', 1568 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/suisei-kawaii.mp3', 1569 | audioInfo: 'https://youtu.be/Xn6fzMAOZFs?t=194 & https://youtu.be/i7oDE-5Q-5o?t=275' 1570 | }, 1571 | { 1572 | modeName: 'ressha', 1573 | imgNormalUrl: 'https://i.imgur.com/xy2RLDJ.png', 1574 | imgPopUrl: 'https://i.imgur.com/7EoGqLX.png', 1575 | imgIconUrl: 'https://i.imgur.com/bvF03rT.png', 1576 | imgInfo: 'https://youtu.be/ZWTuQnb9wq0?t=21 & https://youtu.be/ZWTuQnb9wq0?t=25', 1577 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/suisei-ressha-shu1.mp3', 1578 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/suisei-ressha-shu2.mp3', 1579 | audioInfo: 'https://youtu.be/KGj0re0whzA?t=2214' 1580 | }, 1581 | { 1582 | modeName: 'hi-honey', 1583 | imgNormalUrl: 'https://i.imgur.com/fxjCnhH.png', 1584 | imgPopUrl: 'https://i.imgur.com/w4OJI3r.png', 1585 | imgIconUrl: 'https://i.imgur.com/tKNPCai.png', 1586 | imgInfo: 'https://youtu.be/VTyhCpNSMtc', 1587 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/suisei-hi-honey-hi.mp3', 1588 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/suisei-hi-honey-honey.mp3', 1589 | audioInfo: 'https://youtu.be/MvaMY_92T-c?t=3932' 1590 | }, 1591 | { 1592 | modeName: 'hehehe', 1593 | imgNormalUrl: 'https://i.imgur.com/EjUIZDb.png', 1594 | imgPopUrl: 'https://i.imgur.com/KGaBnA4.png', 1595 | imgIconUrl: 'https://i.imgur.com/D44zTwd.png', 1596 | imgInfo: 'https://youtu.be/mqr1eP25vg4?t=205 & https://youtu.be/mqr1eP25vg4?t=1357', 1597 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/suisei-hehehe1.mp3', 1598 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/suisei-hehehe2.mp3', 1599 | audioInfo: 'https://suisei.moe/ & https://github.com/suisei-cn/sbtn-assets/blob/d51be385f10ebb7d4f89dec9cb2ba9d1efb0ade4/assets/ehhh.mp3' 1600 | } 1601 | ] 1602 | }) 1603 | await WaifuRepo.upsertWaifu({ 1604 | urlId: 'ame', 1605 | name: 'Watson Amelia', 1606 | modeConfigList: [ 1607 | { 1608 | modeName: 'default', 1609 | imgNormalUrl: 'https://i.imgur.com/qKszJAy.jpeg', 1610 | imgPopUrl: 'https://i.imgur.com/MzewfJb.jpeg', 1611 | imgIconUrl: 'https://i.imgur.com/qKszJAy.jpeg', 1612 | imgInfo: '', 1613 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/ame-weee.mp3', 1614 | audioInfo: 'https://youtu.be/f2lYmGRQq9M?t=1207' 1615 | } 1616 | ] 1617 | }) 1618 | await WaifuRepo.upsertWaifu({ 1619 | urlId: 'lei-on-lion', 1620 | name: '莉安Lion', 1621 | modeConfigList: [ 1622 | { 1623 | modeName: 'default', 1624 | imgNormalUrl: 'https://i.imgur.com/AsHGNUz.jpg', 1625 | imgPopUrl: 'https://i.imgur.com/hYfQXpL.jpg', 1626 | imgIconUrl: 'https://i.imgur.com/5K6BKvT.png', 1627 | imgInfo: 'from her discord server', 1628 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/lei-on-lion-pop.mp3', 1629 | audioInfo: 'https://youtu.be/HDaJXxvmDmE' 1630 | } 1631 | ] 1632 | }) 1633 | await WaifuRepo.upsertWaifu({ 1634 | urlId: 'megumin', 1635 | name: 'めぐみん', 1636 | modeConfigList: [ 1637 | { 1638 | modeName: 'default', 1639 | imgNormalUrl: 'https://i.imgur.com/s8H5dIH.jpg', 1640 | imgPopUrl: 'https://i.imgur.com/ndhrghU.png', 1641 | imgIconUrl: 'https://i.imgur.com/lH5CqtV.png', 1642 | imgInfo: 'https://youtu.be/eKEgWoxSLl4?t=6 & https://youtu.be/Q4FQUMcYqiQ?t=10', 1643 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/megumin-explosion.mp3', 1644 | audioInfo: 'https://youtu.be/Q4FQUMcYqiQ?t=2' 1645 | } 1646 | ] 1647 | }) 1648 | await WaifuRepo.upsertWaifu({ 1649 | urlId: 'aqua', 1650 | name: 'あくあ', 1651 | modeConfigList: [ 1652 | { 1653 | modeName: 'default', 1654 | imgNormalUrl: 'https://i.imgur.com/P8KGJFp.jpg', 1655 | imgPopUrl: 'https://i.imgur.com/mc2h9sC.jpg', 1656 | imgIconUrl: 'https://i.imgur.com/tBvuWEC.png', 1657 | imgInfo: '', 1658 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/aqua-pop.mp3', 1659 | audioInfo: 'https://youtu.be/HjjKAsi6pis?t=57' 1660 | } 1661 | ] 1662 | }) 1663 | await WaifuRepo.upsertWaifu({ 1664 | urlId: 'watame', 1665 | name: '角巻わため', 1666 | modeConfigList: [ 1667 | { 1668 | modeName: 'default', 1669 | imgNormalUrl: 'https://i.imgur.com/tiTPdnY.png', 1670 | imgPopUrl: 'https://i.imgur.com/e7W3LdL.png', 1671 | imgIconUrl: 'https://i.imgur.com/ZnXi79M.png', 1672 | imgInfo: 'https://virtualyoutuber.fandom.com/wiki/Tsunomaki_Watame', 1673 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/watame-pop.mp3', 1674 | audioInfo: 'https://youtu.be/x6hem8vmH4M' 1675 | } 1676 | ] 1677 | }) 1678 | await WaifuRepo.upsertWaifu({ 1679 | urlId: 'riksa', 1680 | name: 'Riksa Dhirendra', 1681 | modeConfigList: [ 1682 | { 1683 | modeName: 'default', 1684 | imgNormalUrl: 'https://i.imgur.com/aXx7qze.jpg', 1685 | imgPopUrl: 'https://i.imgur.com/3djOLsw.jpg', 1686 | imgIconUrl: 'https://i.imgur.com/aXx7qze.jpg', 1687 | imgInfo: 'https://twitter.com/RiksaDhirendra/status/1265173609724567552', 1688 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/riksa-pop.mp3', 1689 | audioInfo: 'https://youtu.be/L7rTTbibXB8' 1690 | } 1691 | ] 1692 | }) 1693 | await WaifuRepo.upsertWaifu({ 1694 | urlId: 'scaramouche', 1695 | name: 'Scaramouche (genshinimpact)', 1696 | modeConfigList: [ 1697 | { 1698 | modeName: 'default', 1699 | imgNormalUrl: 'https://i.imgur.com/UpIXqhv.jpeg', 1700 | imgPopUrl: 'https://i.imgur.com/33JfTs9.jpeg', 1701 | imgIconUrl: 'https://i.imgur.com/yAEl6Et.png', 1702 | imgInfo: 'The cut scenes from Genshinimpact ', 1703 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/scaramouche-pop.mp3', 1704 | audioInfo: 'Game Genshinimpact' 1705 | } 1706 | ] 1707 | }) 1708 | await WaifuRepo.upsertWaifu({ 1709 | urlId: 'toba-rana', 1710 | name: '鳥羽樂奈', 1711 | modeConfigList: [ 1712 | { 1713 | modeName: 'default', 1714 | imgNormalUrl: 'https://i.imgur.com/LYUWs56.jpeg', 1715 | imgPopUrl: 'https://i.imgur.com/cTGhYB7.jpeg', 1716 | imgIconUrl: 'https://i.imgur.com/nxLdZFe.png', 1717 | imgInfo: 'https://www.facebook.com/RanaVtb/photos/?tab=album&album_id=111705640746455&ref=page_internal&mt_nav=1', 1718 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/toba-rana-pop.mp3', 1719 | audioInfo: 'https://youtu.be/qgWxzQQT0ew' 1720 | } 1721 | ] 1722 | }) 1723 | await WaifuRepo.upsertWaifu({ 1724 | urlId: 'kiyoshi', 1725 | name: '夏樹きよし', 1726 | modeConfigList: [ 1727 | { 1728 | modeName: 'default', 1729 | imgNormalUrl: 'https://i.imgur.com/tE6VoVu.png', 1730 | imgPopUrl: 'https://i.imgur.com/WMybtrq.png', 1731 | imgIconUrl: 'https://i.imgur.com/qgbsAan.png', 1732 | imgInfo: 'https://youtube.com/channel/UCApwCqmHCddqQkAObr-CqdQ', 1733 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/kiyoshi-pop.mp3', 1734 | audioInfo: 'https://youtu.be/75V4i_IZBXE' 1735 | } 1736 | ] 1737 | }) 1738 | await WaifuRepo.upsertWaifu({ 1739 | urlId: 'chengmi', 1740 | name: '橙米', 1741 | modeConfigList: [ 1742 | { 1743 | modeName: 'default', 1744 | imgNormalUrl: 'https://i.imgur.com/DYlk6fR.png', 1745 | imgPopUrl: 'https://i.imgur.com/8pFwzBL.png', 1746 | imgIconUrl: 'https://i.imgur.com/lwx6QBf.png', 1747 | imgInfo: 'from herself', 1748 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/chengmi-pop.mp3', 1749 | audioInfo: 'from herself' 1750 | } 1751 | ] 1752 | }) 1753 | await WaifuRepo.upsertWaifu({ 1754 | urlId: 'toyakenmochi', 1755 | name: '剣持刀也', 1756 | modeConfigList: [ 1757 | { 1758 | modeName: 'default', 1759 | imgNormalUrl: 'https://i.imgur.com/dKETUP8.jpeg', 1760 | imgPopUrl: 'https://i.imgur.com/lQcW1FH.jpeg', 1761 | imgIconUrl: 'https://i.imgur.com/4o2DFgv.png', 1762 | imgInfo: 'https://twitter.com/rei_toya_rei/status/1419696268561948678' 1763 | } 1764 | ] 1765 | }) 1766 | await WaifuRepo.upsertWaifu({ 1767 | urlId: 'kasasagi', 1768 | name: '姬野鵲', 1769 | modeConfigList: [ 1770 | { 1771 | modeName: 'default', 1772 | imgNormalUrl: 'https://i.imgur.com/tEL1MOi.jpg', 1773 | imgPopUrl: 'https://i.imgur.com/DSFgVFz.jpg', 1774 | imgIconUrl: 'https://i.imgur.com/hl2gxWK.png', 1775 | imgInfo: 'https://youtu.be/_8E0N6SjZvo', 1776 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/kasasagi-pop.mp3', 1777 | audioInfo: 'https://youtu.be/_8E0N6SjZvo' 1778 | } 1779 | ] 1780 | }) 1781 | await WaifuRepo.upsertWaifu({ 1782 | urlId: 'engineer-teamfortress', 1783 | name: 'Engineer', 1784 | modeConfigList: [ 1785 | { 1786 | modeName: 'default', 1787 | imgNormalUrl: 'https://i.imgur.com/MaXM6FG.jpg', 1788 | imgPopUrl: 'https://i.imgur.com/d0ZTqst.jpg', 1789 | imgIconUrl: 'https://i.imgur.com/muaku4V.png', 1790 | imgInfo: 'https://youtu.be/gvdf5n-zI14', 1791 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/engineer-teamfortress-nope.mp3', 1792 | audioInfo: 'https://wiki.teamfortress.com/w/images/b/bd/Engineer_no01.wav' 1793 | } 1794 | ] 1795 | }) 1796 | await WaifuRepo.upsertWaifu({ 1797 | urlId: 'tojiro-genzuki', 1798 | name: '弦月藤士郎', 1799 | modeConfigList: [ 1800 | { 1801 | modeName: 'default', 1802 | imgNormalUrl: 'https://i.imgur.com/GIKan10.png', 1803 | imgPopUrl: 'https://i.imgur.com/Mn2FyCV.png', 1804 | imgIconUrl: 'https://i.imgur.com/M9S3Ldo.png', 1805 | imgInfo: 'https://twitter.com/1O46V/status/1246401408854720519', 1806 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/tojiro-genzuki-pop.mp3', 1807 | audioInfo: 'https://youtu.be/tiMt4d6Xgyc' 1808 | } 1809 | ] 1810 | }) 1811 | await WaifuRepo.upsertWaifu({ 1812 | urlId: 'artemis', 1813 | name: 'Artemis', 1814 | modeConfigList: [ 1815 | { 1816 | modeName: 'default', 1817 | imgNormalUrl: 'https://i.imgur.com/OU9x7Iz.png', 1818 | imgPopUrl: 'https://i.imgur.com/MGPG5DK.png', 1819 | imgIconUrl: 'https://i.imgur.com/MGPG5DK.png', 1820 | imgInfo: 'https://youtu.be/Ww8fbtFgxPY?t=384', 1821 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/artemis-pop.mp3', 1822 | audioInfo: 'https://youtu.be/Ww8fbtFgxPY?t=422' 1823 | } 1824 | ] 1825 | }) 1826 | await WaifuRepo.upsertWaifu({ 1827 | urlId: 'fujinokuma', 1828 | name: '藤乃熊', 1829 | modeConfigList: [ 1830 | { 1831 | modeName: 'default', 1832 | imgNormalUrl: 'https://i.imgur.com/NQ0AaH5.png', 1833 | imgPopUrl: 'https://i.imgur.com/tw6eef0.png', 1834 | imgIconUrl: 'https://i.imgur.com/tw6eef0.png', 1835 | imgInfo: '', 1836 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/fujinokuma-pop.mp3', 1837 | audioInfo: '' 1838 | } 1839 | ] 1840 | }) 1841 | await WaifuRepo.upsertWaifu({ 1842 | urlId: 'yoshizuki-meguru', 1843 | name: 'ヨシヅキ参謀', 1844 | modeConfigList: [ 1845 | { 1846 | modeName: 'default', 1847 | imgNormalUrl: 'https://i.imgur.com/nkWCEYV.png', 1848 | imgPopUrl: 'https://i.imgur.com/SqYKRx0.png', 1849 | imgIconUrl: 'https://i.imgur.com/3FWAZWU.png', 1850 | imgInfo: 'https://youtu.be/8Fb5l02xPJc 08:30 & 14:33', 1851 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/yoshizuki-meguru-pop.mp3', 1852 | audioInfo: 'https://youtu.be/elDHHMzFQmw' 1853 | } 1854 | ] 1855 | }) 1856 | await WaifuRepo.upsertWaifu({ 1857 | urlId: 'arashi', 1858 | name: '神無月嵐', 1859 | modeConfigList: [ 1860 | { 1861 | modeName: 'default', 1862 | imgNormalUrl: 'https://i.imgur.com/BpTq2oC.png', 1863 | imgPopUrl: 'https://i.imgur.com/AgH4h6J.png', 1864 | imgIconUrl: 'https://i.imgur.com/zv7ZroT.png', 1865 | imgInfo: 'https://youtu.be/yBdQ5TEfi_Y', 1866 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/arashi-pop.mp3', 1867 | audioInfo: 'from stream' 1868 | } 1869 | ] 1870 | }) 1871 | await WaifuRepo.upsertWaifu({ 1872 | urlId: 'mayuri-shiina', 1873 | name: '椎名 まゆり', 1874 | modeConfigList: [ 1875 | { 1876 | modeName: 'default', 1877 | imgNormalUrl: 'https://i.imgur.com/ObXK6DV.png', 1878 | imgPopUrl: 'https://i.imgur.com/H69aiOS.png', 1879 | imgIconUrl: 'https://i.imgur.com/HPa2YTk.png', 1880 | imgInfo: 'Steins;Gate 01 9:54', 1881 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/mayuri-shiina-pop.mp3', 1882 | audioInfo: 'Steins;Gate 01 9:54' 1883 | } 1884 | ] 1885 | }) 1886 | await WaifuRepo.upsertWaifu({ 1887 | urlId: 'shellin', 1888 | name: 'シェリン', 1889 | modeConfigList: [ 1890 | { 1891 | modeName: 'default', 1892 | imgNormalUrl: 'https://i.imgur.com/oo5UkYC.jpeg', 1893 | imgPopUrl: 'https://i.imgur.com/2PDLiDf.jpeg', 1894 | imgIconUrl: 'https://i.imgur.com/w9ixUHU.png', 1895 | imgInfo: 'https://youtu.be/6Pd0xCRW7aU?t=2333', 1896 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/shellin-pop.mp3', 1897 | audioInfo: 'https://youtu.be/mOaY7pqOpn4?t=8146' 1898 | } 1899 | ] 1900 | }) 1901 | await WaifuRepo.upsertWaifu({ 1902 | urlId: 'toko-inui', 1903 | name: '戌亥とこ', 1904 | modeConfigList: [ 1905 | { 1906 | modeName: 'default', 1907 | imgNormalUrl: 'https://i.imgur.com/wZi6mM3.jpeg', 1908 | imgPopUrl: 'https://i.imgur.com/wmpqhPm.jpeg', 1909 | imgIconUrl: 'https://i.imgur.com/wmpqhPm.jpeg', 1910 | imgInfo: 'https://youtu.be/_5kOWmcvfa0', 1911 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/toko-inui-pop.mp3', 1912 | audioInfo: 'https://youtu.be/CMK7d-JV4fQ' 1913 | } 1914 | ] 1915 | }) 1916 | await WaifuRepo.upsertWaifu({ 1917 | urlId: 'misaka', 1918 | name: '御坂美琴', 1919 | modeConfigList: [ 1920 | { 1921 | modeName: 'default', 1922 | imgNormalUrl: 'https://i.imgur.com/WNDze6L.jpg', 1923 | imgPopUrl: 'https://i.imgur.com/J0FT4y7.png', 1924 | imgIconUrl: 'https://i.imgur.com/GjMoAzd.png', 1925 | imgInfo: 'A Certain Scientific Railgun T 21 07:50', 1926 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/misaka-bilibili.mp3', 1927 | audioInfo: 'A Certain Magical Index 01 19:05' 1928 | } 1929 | ] 1930 | }) 1931 | await WaifuRepo.upsertWaifu({ 1932 | urlId: 'proose', 1933 | name: '布魯斯', 1934 | modeConfigList: [ 1935 | { 1936 | modeName: 'default', 1937 | imgNormalUrl: 'https://i.imgur.com/Bsb3vXc.jpeg', 1938 | imgPopUrl: 'https://i.imgur.com/AyQUUAn.jpg', 1939 | imgIconUrl: 'https://i.imgur.com/Bsb3vXc.jpeg', 1940 | imgInfo: 'https://twitter.com/purusu0325', 1941 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/proose-pop.mp3', 1942 | audioInfo: 'https://youtu.be/6DhLk_3qypk?t=30' 1943 | } 1944 | ] 1945 | }) 1946 | await WaifuRepo.upsertWaifu({ 1947 | urlId: 'shiroisatou', 1948 | name: '白色砂糖シロイサトウ', 1949 | modeConfigList: [ 1950 | { 1951 | modeName: 'default', 1952 | imgNormalUrl: 'https://i.imgur.com/yckAwkR.jpeg', 1953 | imgPopUrl: 'https://i.imgur.com/fdwVTQz.jpeg', 1954 | imgIconUrl: 'https://i.imgur.com/yckAwkR.jpeg', 1955 | imgInfo: 'https://www.youtube.com/c/%E7%99%BD%E8%89%B2%E7%A0%82%E7%B3%96%E3%82%B7%E3%83%AD%E3%82%A4%E3%82%B5%E3%83%88%E3%82%A6/videos' 1956 | } 1957 | ] 1958 | }) 1959 | await WaifuRepo.upsertWaifu({ 1960 | urlId: 'fuwaminato', 1961 | name: '不破湊', 1962 | modeConfigList: [ 1963 | { 1964 | modeName: 'default', 1965 | imgNormalUrl: 'https://i.imgur.com/pdVzoCx.png', 1966 | imgPopUrl: 'https://i.imgur.com/LF70VvG.png', 1967 | imgIconUrl: 'https://i.imgur.com/R6JuFrp.jpg', 1968 | imgInfo: 'https://youtu.be/qEnOCFh6v-E?t=1500', 1969 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/fuwaminato-pop.mp3', 1970 | audioInfo: 'https://youtu.be/DMC2lq5PpRM' 1971 | } 1972 | ] 1973 | }) 1974 | await WaifuRepo.upsertWaifu({ 1975 | urlId: 'axia-krone', 1976 | name: 'アクシア・クローネ', 1977 | modeConfigList: [ 1978 | { 1979 | modeName: 'default', 1980 | imgNormalUrl: 'https://i.imgur.com/Gmi0fNQ.jpeg', 1981 | imgPopUrl: 'https://i.imgur.com/LKgXzQS.jpeg', 1982 | imgIconUrl: 'https://i.imgur.com/mBj7ds0.jpg', 1983 | imgInfo: 'https://youtu.be/0WdiSlrvmDk?t=116', 1984 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/axia-krone-pop.mp3', 1985 | audioInfo: 'https://youtu.be/kkKdenmXcX4' 1986 | } 1987 | ] 1988 | }) 1989 | await WaifuRepo.upsertWaifu({ 1990 | urlId: 'healing', 1991 | name: 'Healing Ch.希靈', 1992 | modeConfigList: [ 1993 | { 1994 | modeName: 'default', 1995 | imgNormalUrl: 'https://i.imgur.com/Xzpf4Te.jpeg', 1996 | imgPopUrl: 'https://i.imgur.com/hoq5A5j.jpeg', 1997 | imgIconUrl: 'https://i.imgur.com/QBPV7DI.jpg', 1998 | imgInfo: 'https://home.gamer.com.tw/artwork.php?sn=5097688', 1999 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/healing-pop.mp3', 2000 | audioInfo: 'https://youtu.be/J0Opg0RaUFY' 2001 | } 2002 | ] 2003 | }) 2004 | await WaifuRepo.upsertWaifu({ 2005 | urlId: 'duca', 2006 | name: 'Hibiki Du Ca', 2007 | modeConfigList: [ 2008 | { 2009 | modeName: 'default', 2010 | imgNormalUrl: 'https://i.imgur.com/7iSHzw1.png', 2011 | imgPopUrl: 'https://i.imgur.com/mPbbuH9.png', 2012 | imgIconUrl: 'https://i.imgur.com/Gj5RlA5.jpg', 2013 | imgInfo: 'https://youtu.be/p51k0fTQs6k', 2014 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/duca-pop.mp3', 2015 | audioInfo: 'https://youtu.be/p51k0fTQs6k' 2016 | } 2017 | ] 2018 | }) 2019 | await WaifuRepo.upsertWaifu({ 2020 | urlId: 'catandcanned', 2021 | name: '小羯貓貓', 2022 | modeConfigList: [ 2023 | { 2024 | modeName: 'default', 2025 | imgNormalUrl: 'https://i.imgur.com/BvgFmyH.png', 2026 | imgPopUrl: 'https://i.imgur.com/MCPymKB.png', 2027 | imgIconUrl: 'https://i.imgur.com/Qmx8aoC.jpg', 2028 | imgInfo: 'https://youtu.be/H1bcoSBHjEc?t=10698', 2029 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/catandcanned-pop-1.mp3', 2030 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/catandcanned-pop-2.mp3', 2031 | audioInfo: 'https://youtu.be/H1bcoSBHjEc?t=10698' 2032 | } 2033 | ] 2034 | }) 2035 | await WaifuRepo.upsertWaifu({ 2036 | urlId: 'izuru', 2037 | name: '奏手イヅル', 2038 | modeConfigList: [ 2039 | { 2040 | modeName: 'default', 2041 | imgNormalUrl: 'https://i.imgur.com/HIXyWLY.jpg', 2042 | imgPopUrl: 'https://i.imgur.com/3byTEyE.jpg', 2043 | imgIconUrl: 'https://i.imgur.com/zOVrVEN.jpg', 2044 | imgInfo: 'https://youtu.be/c9zeQriSTAU', 2045 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/izuru-pop.mp3', 2046 | audioInfo: 'https://twitter.com/i/status/1390107245703307266' 2047 | }, 2048 | { 2049 | modeName: '2', 2050 | imgNormalUrl: 'https://i.imgur.com/HI8SA2p.jpeg', 2051 | imgPopUrl: 'https://i.imgur.com/KpBVQdD.jpeg', 2052 | imgIconUrl: 'https://i.imgur.com/fbbfMIn.jpg', 2053 | imgInfo: 'https://youtu.be/rnTQ47UOCrw?t=148 & https://youtu.be/NM6Qi1PM2Kw?t=1146', 2054 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/izuru-2-pop.mp3', 2055 | audioInfo: 'https://youtu.be/NM6Qi1PM2Kw?t=1596' 2056 | } 2057 | ] 2058 | }) 2059 | await WaifuRepo.upsertWaifu({ 2060 | urlId: 'petra', 2061 | name: 'Petra Gurin', 2062 | modeConfigList: [ 2063 | { 2064 | modeName: 'default', 2065 | imgNormalUrl: 'https://i.imgur.com/wy5CByt.jpeg', 2066 | imgPopUrl: 'https://i.imgur.com/tdvuFOK.png', 2067 | imgIconUrl: 'https://i.imgur.com/hqdQaR4.jpg', 2068 | imgInfo: 'https://www.youtube.com/channel/UCgA2jKRkqpY_8eysPUs8sjw (Profile pic) & https://twitter.com/petra_gurin/status/1418611909247471619', 2069 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/petra-pop.mp3', 2070 | audioInfo: 'https://youtu.be/VOAOtZ5xPng' 2071 | } 2072 | ] 2073 | }) 2074 | await WaifuRepo.upsertWaifu({ 2075 | urlId: 'aonezutarou', 2076 | name: '葵鼠たろう', 2077 | modeConfigList: [ 2078 | { 2079 | modeName: 'default', 2080 | imgNormalUrl: 'https://i.imgur.com/o3366oI.png', 2081 | imgPopUrl: 'https://i.imgur.com/4WUSGkG.png', 2082 | imgIconUrl: 'https://i.imgur.com/AX09Zbd.jpg', 2083 | imgInfo: 'from himself', 2084 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/aonezutarou-pop.mp3', 2085 | audioInfo: 'https://youtu.be/muXxTI08hls?t=45' 2086 | } 2087 | ] 2088 | }) 2089 | await WaifuRepo.upsertWaifu({ 2090 | urlId: 'ollie', 2091 | name: 'Kureiji Ollie', 2092 | modeConfigList: [ 2093 | { 2094 | modeName: 'default', 2095 | imgNormalUrl: 'https://i.imgur.com/bUDLayJ.png', 2096 | imgPopUrl: 'https://i.imgur.com/8hTYDnX.png', 2097 | imgIconUrl: 'https://i.imgur.com/sQhawan.png', 2098 | imgInfo: 'https://youtu.be/kzNzUK3xQto?t=1773', 2099 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/ollie-kukuku1.mp3', 2100 | audioNormalUrl: 'https://softwaresing.github.io/popwaifu-file/audio/ollie-kukuku2.mp3', 2101 | audioInfo: 'https://youtu.be/HT9UrfTYsB4?t=161' 2102 | } 2103 | ] 2104 | }) 2105 | await WaifuRepo.upsertWaifu({ 2106 | urlId: 'amaha-ari', 2107 | name: '天葉亞里', 2108 | modeConfigList: [ 2109 | { 2110 | modeName: 'default', 2111 | imgNormalUrl: 'https://i.imgur.com/rNMZ1dz.jpg', 2112 | imgPopUrl: 'https://i.imgur.com/8WhVGqf.jpg', 2113 | imgIconUrl: 'https://i.imgur.com/vW2TjJH.jpg', 2114 | imgInfo: 'https://youtu.be/HVu67hLqJQo', 2115 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/amaha-ari-pop.mp3', 2116 | audioInfo: 'https://youtu.be/HVu67hLqJQo' 2117 | } 2118 | ] 2119 | }) 2120 | await WaifuRepo.upsertWaifu({ 2121 | urlId: 'lutra', 2122 | name: '露恰露恰', 2123 | modeConfigList: [ 2124 | { 2125 | modeName: 'default', 2126 | imgNormalUrl: 'https://i.imgur.com/wZ4QiDW.png', 2127 | imgPopUrl: 'https://i.imgur.com/oPObhTE.png', 2128 | imgIconUrl: 'https://i.imgur.com/CotAGfV.png', 2129 | imgInfo: 'https://youtu.be/y54UfL3OtfI?t=1556 & https://youtu.be/y54UfL3OtfI?t=1605', 2130 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/lutra-hota.mp3', 2131 | audioInfo: 'https://youtu.be/y54UfL3OtfI?t=1604' 2132 | } 2133 | ] 2134 | }) 2135 | await WaifuRepo.upsertWaifu({ 2136 | urlId: 'obear', 2137 | name: '歐貝爾', 2138 | modeConfigList: [ 2139 | { 2140 | modeName: 'default', 2141 | imgNormalUrl: 'https://i.imgur.com/nfkWJzA.png', 2142 | imgPopUrl: 'https://i.imgur.com/42qR9cM.png', 2143 | imgIconUrl: 'https://i.imgur.com/SZ5bRdu.png', 2144 | imgInfo: 'https://youtu.be/Fl8QAebF0B0', 2145 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/obear-yuema.mp3', 2146 | audioInfo: 'https://youtu.be/Fl8QAebF0B0' 2147 | } 2148 | ] 2149 | }) 2150 | await WaifuRepo.upsertWaifu({ 2151 | urlId: 'chilla', 2152 | name: '祈菈‧貝希毛絲', 2153 | modeConfigList: [ 2154 | { 2155 | modeName: 'default', 2156 | imgNormalUrl: 'https://i.imgur.com/rvVzvSH.png', 2157 | imgPopUrl: 'https://i.imgur.com/WSy4pYb.png', 2158 | imgIconUrl: 'https://i.imgur.com/My2Cf0M.png', 2159 | imgInfo: 'https://youtu.be/46bMAQHyui8?t=3037 & https://youtu.be/46bMAQHyui8?t=3041', 2160 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/chilla-bab.mp3', 2161 | audioInfo: 'https://youtu.be/upGYnCMLIMw' 2162 | }, 2163 | { 2164 | modeName: 'bababa', 2165 | imgNormalUrl: 'https://i.imgur.com/rvVzvSH.png', 2166 | imgPopUrl: 'https://i.imgur.com/WSy4pYb.png', 2167 | imgIconUrl: 'https://i.imgur.com/My2Cf0M.png', 2168 | imgInfo: 'https://youtu.be/46bMAQHyui8?t=3037 & https://youtu.be/46bMAQHyui8?t=3041', 2169 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/chilla-bababa.mp3', 2170 | audioInfo: 'https://youtu.be/upGYnCMLIMw' 2171 | } 2172 | ] 2173 | }) 2174 | await WaifuRepo.upsertWaifu({ 2175 | urlId: 'hati', 2176 | name: '哈提Hati', 2177 | modeConfigList: [ 2178 | { 2179 | modeName: 'default', 2180 | imgNormalUrl: 'https://i.imgur.com/exX5Xmi.jpeg', 2181 | imgPopUrl: 'https://i.imgur.com/q4qEBAB.jpeg', 2182 | imgIconUrl: 'https://i.imgur.com/exX5Xmi.jpeg', 2183 | imgInfo: 'https://twitter.com/MoonHoundHati', 2184 | audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/hati-pop.mp3', 2185 | audioInfo: 'https://discord.com/channels/785078538419699723/857133583651438623/857133759672614923' 2186 | } 2187 | ] 2188 | }) 2189 | 2190 | // await WaifuRepo.upsertWaifu({ 2191 | // urlId: '', 2192 | // name: '', 2193 | // modeConfigList: [ 2194 | // { 2195 | // modeName: 'default', 2196 | // imgNormalUrl: '', 2197 | // imgPopUrl: '', 2198 | // imgIconUrl: '', 2199 | // imgInfo: '', 2200 | // audioPopUrl: 'https://softwaresing.github.io/popwaifu-file/audio/.mp3', 2201 | // audioInfo: '' 2202 | // } 2203 | // ] 2204 | // }) 2205 | // for (let i = 0; i < 200; i += 1) { 2206 | // await WaifuRepo.upsertWaifu({ 2207 | // urlId: `${Math.random()}${Date.now()}`.slice(2), 2208 | // name: `${Math.random()}`, 2209 | // modeConfigList: [ 2210 | // { 2211 | // modeName: 'default', 2212 | // imgNormalUrl: 'https://i.imgur.com/w50ILOk.jpg', 2213 | // imgPopUrl: 'https://i.imgur.com/8jOQjgL.jpg', 2214 | // imgInfo: `${Date.now()}${Math.random()}` 2215 | // } 2216 | // ] 2217 | // }) 2218 | // } 2219 | } 2220 | 2221 | run() 2222 | -------------------------------------------------------------------------------- /dev-script/syncIndex.js: -------------------------------------------------------------------------------- 1 | const Mongodb = require('~common/connection/Mongodb') 2 | const WaifuModel = require('~entity/waifu/WaifuModel') 3 | 4 | async function run () { 5 | await Mongodb.connect() 6 | await syncIndex() 7 | await Mongodb.disconnect() 8 | } 9 | 10 | async function syncIndex () { 11 | for (const model of [WaifuModel]) { 12 | await model.syncIndexes() 13 | } 14 | } 15 | 16 | run() 17 | -------------------------------------------------------------------------------- /dev-script/updateSchema.js: -------------------------------------------------------------------------------- 1 | const Mongodb = require('~common/connection/Mongodb') 2 | const WaifuModel = require('~entity/waifu/WaifuModel') 3 | 4 | async function run () { 5 | await Mongodb.connect() 6 | await schema001() 7 | await Mongodb.disconnect() 8 | } 9 | 10 | async function schema001 () { 11 | await WaifuModel.updateMany( 12 | { schemaVersion: '001.000.000' }, 13 | { 14 | $set: { 15 | 'modeConfigList.$[].imgIconUrl': '', 16 | schemaVersion: '001.001.000' 17 | } 18 | } 19 | ) 20 | } 21 | 22 | run() 23 | -------------------------------------------------------------------------------- /ecosystem.cron.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [{ 3 | name: 'cron-popwaifu', 4 | script: 'src/cron/main.js', 5 | instances: 1, 6 | autorestart: true, 7 | watch: false, 8 | kill_timeout: 60 * 1000, 9 | max_memory_restart: '512M', 10 | exec_mode: 'fork', 11 | env: { 12 | NODE_ENV: 'development' 13 | }, 14 | env_production: { 15 | NODE_ENV: 'production' 16 | } 17 | }] 18 | } 19 | -------------------------------------------------------------------------------- /ecosystem.server.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [{ 3 | name: 'server-popwaifu', 4 | script: 'src/server/main.js', 5 | instances: 'max', 6 | autorestart: true, 7 | watch: false, 8 | kill_timeout: 20000, 9 | max_memory_restart: '1024M', 10 | exec_mode: 'cluster', 11 | env: { 12 | NODE_ENV: 'development', 13 | PORT: 3000 14 | }, 15 | env_production: { 16 | NODE_ENV: 'production', 17 | PORT: 3000 18 | } 19 | }] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "popwaifu", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "build_web": "sh build-web.sh", 9 | "cron_dev": "NODE_ENV=development node ./src/cron/main.js", 10 | "cron_pm2_dev": "pm2 reload ecosystem.cron.config.js", 11 | "cron_pm2_prod": "pm2 reload ecosystem.cron.config.js --env production", 12 | "server_dev": "NODE_ENV=development node ./src/server/main.js", 13 | "server_pm2_prod": "pm2 reload ecosystem.server.config.js --env production", 14 | "server_deploy_prod": "npm i && npm run build_web && npm run server_pm2_prod", 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "author": "SoftwareSing", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@apidevtools/swagger-parser": "^10.0.3", 21 | "bridge-redis": "0.0.2", 22 | "cache-bridge": "0.0.4", 23 | "cors": "^2.8.5", 24 | "express": "^4.17.3", 25 | "ipaddr.js": "^2.0.1", 26 | "mongoose": "^6.2.8", 27 | "node-cron": "^3.0.0", 28 | "redis": "^3.1.2", 29 | "svelte": "^3.46.4", 30 | "swagger-ui-express": "^4.3.0", 31 | "~common": "file:src/common", 32 | "~config": "file:config", 33 | "~entity": "file:src/entity", 34 | "~server": "file:src/server" 35 | }, 36 | "devDependencies": { 37 | "@types/express": "^4.17.13", 38 | "eslint": "^8.13.0", 39 | "eslint-config-standard": "^17.0.0", 40 | "eslint-plugin-import": "^2.26.0", 41 | "eslint-plugin-n": "^15.1.0", 42 | "eslint-plugin-promise": "^6.0.0", 43 | "eslint-plugin-yml": "^1.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/common/checker/id.js: -------------------------------------------------------------------------------- 1 | function isValidId (id) { 2 | return typeof id === 'string' && /^[0-9a-fA-F]{24}$/.test(id) 3 | } 4 | 5 | module.exports = { 6 | isValidId 7 | } 8 | -------------------------------------------------------------------------------- /src/common/checker/object.js: -------------------------------------------------------------------------------- 1 | function isObject (object) { 2 | return object !== null && typeof object === 'object' 3 | } 4 | 5 | module.exports = { 6 | isObject 7 | } 8 | -------------------------------------------------------------------------------- /src/common/connection/Mongodb.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const { mongoUrl } = require('~config/mongodbConfig') 3 | 4 | exports.connect = async function () { 5 | try { 6 | const result = await mongoose.connect(mongoUrl, { 7 | autoIndex: false 8 | }) 9 | console.log('MongoDB connect successful') 10 | return result 11 | } catch (err) { 12 | console.log('MongoDB connection failed') 13 | console.log(err) 14 | process.exit(1) 15 | } 16 | } 17 | 18 | exports.disconnect = async function () { 19 | await mongoose.connection.close(false) 20 | console.log('MongoDB connection closed') 21 | } 22 | -------------------------------------------------------------------------------- /src/common/connection/redis.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require('util') 2 | const redis = require('redis') 3 | const cacheBridge = require('cache-bridge') 4 | const RedisCacheClient = require('bridge-redis')(cacheBridge.CacheClient) 5 | 6 | const redisClient = redis.createClient() 7 | const redisCacheClient = new RedisCacheClient({ client: redisClient }) 8 | 9 | exports.disconnectRedis = promisify(redisClient.quit).bind(redisClient) 10 | exports.redisClient = redisClient 11 | exports.redisCacheClient = redisCacheClient 12 | -------------------------------------------------------------------------------- /src/common/error/HttpError.js: -------------------------------------------------------------------------------- 1 | module.exports = class HttpError extends Error { 2 | /** 3 | * @param {Number} statusCode 4 | */ 5 | constructor (statusCode, message = '') { 6 | super(message) 7 | this.statusCode = statusCode 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/error/consoleUnexpectedError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Error} err 3 | */ 4 | exports.consoleUnexpectedError = function (err) { 5 | const line = '----------' 6 | const time = (new Date()).toISOString() 7 | const text = 'unexpected error' 8 | console.error(`${line}\n${time}\n${text}\n${err.stack}\n${line}\n`) 9 | } 10 | -------------------------------------------------------------------------------- /src/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "~common", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /src/common/utils/BackgroundRunner.js: -------------------------------------------------------------------------------- 1 | const { sleep } = require('./sleep') 2 | 3 | /** 4 | * @type {Set} 5 | */ 6 | const set = new Set() 7 | 8 | /** 9 | * @param {Function} callback 10 | */ 11 | exports.run = async function (callback) { 12 | const execPromise = execCallback(callback) 13 | set.add(execPromise) 14 | await execPromise 15 | set.delete(execPromise) 16 | } 17 | 18 | exports.waitAllDone = async function () { 19 | while (set.size > 0) { 20 | await Promise.all([...set]) 21 | await sleep(10) 22 | } 23 | } 24 | 25 | async function execCallback (cb) { 26 | try { 27 | return await cb() 28 | } catch (err) { 29 | console.error(err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/common/utils/sleep.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Number} ms 3 | */ 4 | exports.sleep = function (ms) { 5 | return new Promise((resolve) => { 6 | setTimeout(resolve, ms) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/cron/main.js: -------------------------------------------------------------------------------- 1 | const cron = require('node-cron') 2 | const Mongodb = require('~common/connection/Mongodb') 3 | const { disconnectRedis } = require('~common/connection/redis') 4 | 5 | const taskList = [] 6 | const runningSet = new Set() 7 | 8 | async function main () { 9 | await Mongodb.connect() 10 | 11 | // ┌────────────── second (optional) 0-59 12 | // │ ┌──────────── minute 0-59 13 | // │ │ ┌────────── hour 0-23 14 | // │ │ │ ┌──────── day of month 1-31 15 | // │ │ │ │ ┌────── month 1-12 (or names) 16 | // │ │ │ │ │ ┌──── day of week 0-7 (or names, 0 or 7 are sunday) 17 | // │ │ │ │ │ │ 18 | // │ │ │ │ │ │ 19 | // * * * * * * 20 | 21 | schedule('0 0 * * * *', () => console.log('example cron')) 22 | } 23 | 24 | function schedule (expression, func) { 25 | const task = cron.schedule(expression, willRun(func)) 26 | taskList.push(task) 27 | } 28 | 29 | function willRun (job) { 30 | return async function () { 31 | const jobPromise = job() 32 | runningSet.add(jobPromise) 33 | try { 34 | await jobPromise 35 | } catch (err) { 36 | console.error(`-----\n${new Date().toISOString()}`) 37 | console.error(err) 38 | console.error('-----') 39 | } finally { 40 | runningSet.delete(jobPromise) 41 | } 42 | } 43 | } 44 | 45 | async function close () { 46 | for (const task of taskList) { 47 | task.stop() 48 | } 49 | 50 | console.log(`${new Date().toISOString()} wait all running job finish`) 51 | await Promise.all([...runningSet]) 52 | 53 | console.log(`${new Date().toISOString()} close connection`) 54 | await Promise.all([ 55 | Mongodb.disconnect(), 56 | disconnectRedis() 57 | ]) 58 | } 59 | 60 | main() 61 | process.on('SIGINT', function () { 62 | console.info('SIGINT signal received') 63 | close() 64 | }) 65 | -------------------------------------------------------------------------------- /src/entity/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "~entity", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /src/entity/popLog/PopLog.js: -------------------------------------------------------------------------------- 1 | module.exports = class PopLog { 2 | /** 3 | * @param {Object} obj 4 | * @param {String} obj.popLogId 5 | * @param {String} obj.ip 6 | * @param {Number} obj.popCount 7 | * @param {Date | Number} obj.logTime 8 | */ 9 | constructor ({ popLogId, ip, popCount, logTime }) { 10 | this.popLogId = popLogId 11 | this.ip = ip 12 | this.popCount = popCount 13 | this.logTime = new Date(logTime) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/entity/popLog/PopLogBridge.js: -------------------------------------------------------------------------------- 1 | const { Cache } = require('cache-bridge') 2 | const { redisCacheClient } = require('~common/connection/redis') 3 | const { popLimit } = require('~config/limitConfig') 4 | 5 | const recentLogCache = new Cache(redisCacheClient, { 6 | prefix: 'ipLog', 7 | ttl: popLimit.time 8 | }) 9 | 10 | exports.getIpTimeCount = function (ipTimeKey) { 11 | return recentLogCache.get(ipTimeKey) 12 | } 13 | 14 | exports.writeIpTimeCount = function (ipTimeKey, count) { 15 | return recentLogCache.setNotExist(ipTimeKey, count) 16 | } 17 | -------------------------------------------------------------------------------- /src/entity/popLog/PopLogRepo.js: -------------------------------------------------------------------------------- 1 | const { popLimit } = require('~config/limitConfig') 2 | const HttpError = require('~common/error/HttpError') 3 | 4 | const PopLogBridge = require('./PopLogBridge') 5 | const { buildPopLog, getIpTimeKey } = require('./helper') 6 | 7 | exports.getIpRecentLog = async function (ip) { 8 | const ipTimeKey = getIpTimeKey(ip) 9 | const popCount = await PopLogBridge.getIpTimeCount(ipTimeKey) 10 | if (!popCount) return undefined 11 | 12 | return buildPopLog(ipTimeKey, popCount) 13 | } 14 | 15 | exports.record = async function ({ ip, popCount }) { 16 | if (popCount > popLimit.count) throw new HttpError(403, 'too many') 17 | 18 | const ipTimeKey = getIpTimeKey(ip) 19 | const writeResult = await PopLogBridge.writeIpTimeCount(ipTimeKey, popCount) 20 | if (!writeResult) throw new HttpError(429, 'too fast') 21 | 22 | return buildPopLog(ipTimeKey, popCount) 23 | } 24 | -------------------------------------------------------------------------------- /src/entity/popLog/helper.js: -------------------------------------------------------------------------------- 1 | const PopLog = require('./PopLog') 2 | const { popLimit } = require('~config/limitConfig') 3 | 4 | function buildPopLog (ipTimeKey, count) { 5 | const { ip, time } = getIpAndTimeFromKey(ipTimeKey) 6 | return new PopLog({ popLogId: ipTimeKey, ip, logTime: time, popCount: count }) 7 | } 8 | 9 | function getLogTime () { 10 | const now = Date.now() 11 | return now - (now % popLimit.time) 12 | } 13 | 14 | function getIpTimeKey (ip) { 15 | const logTime = getLogTime() 16 | return `${ip}_${logTime}` 17 | } 18 | 19 | /** 20 | * @param {String} ipTimeKey 21 | */ 22 | function getIpAndTimeFromKey (ipTimeKey) { 23 | const [, ip, timeStr] = ipTimeKey.match(/^(.{1,})_([0-9]{1,})$/) 24 | return { ip, time: Number(timeStr) } 25 | } 26 | 27 | module.exports = { 28 | buildPopLog, 29 | getLogTime, 30 | getIpTimeKey, 31 | getIpAndTimeFromKey 32 | } 33 | -------------------------------------------------------------------------------- /src/entity/waifu/Waifu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} ModeConfig 3 | * @property {String} modeName 4 | * @property {String} imgNormalUrl 5 | * @property {String} imgPopUrl 6 | * @property {String} [imgIconUrl] 7 | * @property {String} [imgInfo] 8 | * @property {String} [audioNormalUrl] 9 | * @property {String} [audioPopUrl] 10 | * @property {String} [audioInfo] 11 | */ 12 | 13 | module.exports = class Waifu { 14 | /** 15 | * @param {Object} obj 16 | * @param {String} obj.waifuId 17 | * @param {String} obj.urlId 18 | * @param {String} obj.name 19 | * @param {Number} obj.popCount 20 | * @param {Array} obj.modeConfigList 21 | */ 22 | constructor ({ waifuId, urlId, name, popCount, modeConfigList }) { 23 | this.waifuId = waifuId 24 | this.urlId = urlId 25 | this.name = name 26 | this.popCount = popCount 27 | this.modeConfigList = modeConfigList 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/entity/waifu/WaifuBridge.js: -------------------------------------------------------------------------------- 1 | const cacheBridge = require('cache-bridge') 2 | const { redisCacheClient } = require('~common/connection/redis') 3 | 4 | const WaifuModel = require('./WaifuModel') 5 | 6 | const defaultProjection = { schemaVersion: 0, createdAt: 0, updatedAt: 0 } 7 | 8 | const { bridge: listBridge } = cacheBridge({ 9 | cacheClient: redisCacheClient, 10 | prefix: 'list', 11 | ttl: 4 * 1000, 12 | get: async function () { 13 | return await WaifuModel.find({}, defaultProjection).lean() 14 | } 15 | }) 16 | 17 | const { bridge: urlIdWaifuBridge } = cacheBridge({ 18 | cacheClient: redisCacheClient, 19 | prefix: 'urlIdWaifu', 20 | ttl: 120 * 1000, 21 | cacheUndefined: true, 22 | ttlForUndefined: 10 * 1000, 23 | get: async function (urlId) { 24 | const obj = await WaifuModel.findOne({ urlId }, defaultProjection).lean() 25 | return obj || undefined 26 | } 27 | }) 28 | 29 | exports.getList = function () { 30 | return listBridge.get('list') 31 | } 32 | 33 | exports.getByUrlId = function (urlId) { 34 | return urlIdWaifuBridge.get(urlId) 35 | } 36 | -------------------------------------------------------------------------------- /src/entity/waifu/WaifuModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const modeConfigSchema = new mongoose.Schema({ 4 | modeName: { 5 | type: String, 6 | required: true 7 | }, 8 | imgNormalUrl: { 9 | type: String, 10 | default: '' 11 | }, 12 | imgPopUrl: { 13 | type: String, 14 | default: '' 15 | }, 16 | imgIconUrl: { 17 | type: String, 18 | default: '' 19 | }, 20 | imgInfo: { 21 | type: String, 22 | default: '' 23 | }, 24 | audioNormalUrl: { 25 | type: String, 26 | default: '' 27 | }, 28 | audioPopUrl: { 29 | type: String, 30 | default: '' 31 | }, 32 | audioInfo: { 33 | type: String, 34 | default: '' 35 | } 36 | }, { _id: false }) 37 | 38 | const schema = new mongoose.Schema({ 39 | schemaVersion: { 40 | type: String, 41 | default: '001.001.000' 42 | }, 43 | urlId: { 44 | type: String, 45 | required: true 46 | }, 47 | name: { 48 | type: String, 49 | default: '' 50 | }, 51 | popCount: { 52 | type: Number, 53 | default: 0 54 | }, 55 | modeConfigList: { 56 | type: [modeConfigSchema], 57 | default: [{ 58 | modeName: 'default', 59 | imgNormalUrl: '', 60 | imgPopUrl: '', 61 | imgIconUrl: '', 62 | imgInfo: '', 63 | audioNormalUrl: '', 64 | audioPopUrl: '', 65 | audioInfo: '' 66 | }] 67 | } 68 | }, { collection: 'waifu', timestamps: true, versionKey: false }) 69 | 70 | schema.index({ popCount: 1 }) 71 | schema.index({ urlId: 1 }, { unique: true }) 72 | 73 | const model = mongoose.model('Waifu', schema) 74 | module.exports = model 75 | -------------------------------------------------------------------------------- /src/entity/waifu/WaifuRepo.js: -------------------------------------------------------------------------------- 1 | const WaifuBridge = require('./WaifuBridge') 2 | const WaifuModel = require('./WaifuModel') 3 | const { buildWaifu } = require('./helper') 4 | const { addWaifusPopCount } = require('./waifuRepoMethod/addWaifusPopCount') 5 | 6 | /** 7 | * @typedef {import('./Waifu').ModeConfig} ModeConfig 8 | */ 9 | 10 | exports.addWaifusPopCount = addWaifusPopCount 11 | 12 | exports.getList = async function () { 13 | /** 14 | * @type {Array} 15 | */ 16 | const objList = await WaifuBridge.getList() 17 | return objList.map(buildWaifu) 18 | } 19 | 20 | exports.getByUrlId = async function (urlId) { 21 | const obj = await WaifuBridge.getByUrlId(urlId) 22 | return obj ? buildWaifu(obj) : undefined 23 | } 24 | 25 | /** 26 | * @param {Object} waifuData 27 | * @param {String} waifuData.urlId 28 | * @param {String} waifuData.name 29 | * @param {Array} waifuData.modeConfigList 30 | */ 31 | exports.upsertWaifu = async function ({ urlId, name, modeConfigList }) { 32 | const obj = await WaifuModel.findOneAndUpdate( 33 | { urlId }, 34 | { name, modeConfigList }, 35 | { new: true, upsert: true, setDefaultsOnInsert: true } 36 | ).lean() 37 | return buildWaifu(obj) 38 | } 39 | -------------------------------------------------------------------------------- /src/entity/waifu/helper.js: -------------------------------------------------------------------------------- 1 | const Waifu = require('./Waifu') 2 | 3 | exports.buildWaifu = function (obj) { 4 | obj.waifuId = String(obj._id) 5 | return new Waifu(obj) 6 | } 7 | -------------------------------------------------------------------------------- /src/entity/waifu/waifuRepoMethod/addWaifusPopCount.js: -------------------------------------------------------------------------------- 1 | const BackgroundRunner = require('~common/utils/BackgroundRunner') 2 | const { sleep } = require('~common/utils/sleep') 3 | const WaifuModel = require('../WaifuModel') 4 | 5 | /** 6 | * @param {Map} waifuPopMap 7 | */ 8 | exports.addWaifusPopCount = async function (waifuPopMap) { 9 | const helper = new IncBulkHelper() 10 | for (const [waifuId, popCount] of waifuPopMap.entries()) { 11 | helper.incPopCount(waifuId, popCount) 12 | } 13 | } 14 | 15 | class IncBulkHelper { 16 | constructor () { 17 | if (IncBulkHelper.instance) return IncBulkHelper.instance 18 | 19 | IncBulkHelper.instance = this 20 | /** 21 | * @type {Map} 22 | */ 23 | this.map = new Map() 24 | 25 | BackgroundRunner.run(async () => { 26 | await sleep(100) 27 | IncBulkHelper.instance = undefined 28 | await this.writeToDb() 29 | }) 30 | } 31 | 32 | incPopCount (waifuId, incCount) { 33 | const count = this.map.get(waifuId) || 0 34 | this.map.set(waifuId, count + incCount) 35 | } 36 | 37 | async writeToDb () { 38 | const writes = [] 39 | for (const [waifuId, popCount] of this.map.entries()) { 40 | writes.push({ 41 | updateOne: { 42 | filter: { _id: waifuId }, 43 | update: { $inc: { popCount } } 44 | } 45 | }) 46 | } 47 | 48 | await WaifuModel.bulkWrite(writes, { ordered: false }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/server/controller/pop/PopController.js: -------------------------------------------------------------------------------- 1 | const { recordPop } = require('./recordPop') 2 | 3 | module.exports = { 4 | recordPop 5 | } 6 | -------------------------------------------------------------------------------- /src/server/controller/pop/recordPop.js: -------------------------------------------------------------------------------- 1 | const HttpError = require('~common/error/HttpError') 2 | const PopLogRepo = require('~entity/popLog/PopLogRepo') 3 | const WaifuRepo = require('~entity/waifu/WaifuRepo') 4 | const { popLimit } = require('~config/limitConfig') 5 | const { isValidId } = require('~common/checker/id') 6 | const { isObject } = require('~common/checker/object') 7 | 8 | exports.recordPop = async function ({ ip, waifuPopObj }) { 9 | const { totalPopCount, waifuPopMap } = getWaifuPopInfoAndCheck(waifuPopObj) 10 | await checkIpLog(ip) 11 | 12 | await PopLogRepo.record({ ip, popCount: totalPopCount }) 13 | await WaifuRepo.addWaifusPopCount(waifuPopMap) 14 | } 15 | 16 | function getWaifuPopInfoAndCheck (waifuPopObj) { 17 | if (!isObject(waifuPopObj)) throw new HttpError(400, 'invalid waifuPopObj') 18 | 19 | const { count: countLimit } = popLimit 20 | const waifuPopMap = new Map() 21 | let totalPopCount = 0 22 | for (const [waifuId, popCount] of Object.entries(waifuPopObj)) { 23 | if (!Number.isSafeInteger(popCount) || popCount < 1) throw new HttpError(400, 'invalid pop count') 24 | if (popCount > countLimit) throw new HttpError(403, 'too many') 25 | 26 | totalPopCount += popCount 27 | if (totalPopCount > countLimit) throw new HttpError(403, 'too many') 28 | 29 | if (!isValidId(waifuId)) throw new HttpError(400, 'invalid waifu id') 30 | waifuPopMap.set(waifuId, popCount) 31 | } 32 | 33 | return { totalPopCount, waifuPopMap } 34 | } 35 | 36 | async function checkIpLog (ip) { 37 | const log = await PopLogRepo.getIpRecentLog(ip) 38 | if (log && Date.now() - log.logTime.getTime() < popLimit.time) { 39 | throw new HttpError(429, 'too many') 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/server/controller/waifu/WaifuController.js: -------------------------------------------------------------------------------- 1 | const { getList } = require('./getList') 2 | const { getPopCountList } = require('./getPopCountList') 3 | 4 | module.exports = { 5 | getList, 6 | getPopCountList 7 | } 8 | -------------------------------------------------------------------------------- /src/server/controller/waifu/getList.js: -------------------------------------------------------------------------------- 1 | const WaifuRepo = require('~entity/waifu/WaifuRepo') 2 | 3 | exports.getList = async function () { 4 | const waifuList = await WaifuRepo.getList() 5 | return waifuList 6 | } 7 | -------------------------------------------------------------------------------- /src/server/controller/waifu/getPopCountList.js: -------------------------------------------------------------------------------- 1 | const WaifuRepo = require('~entity/waifu/WaifuRepo') 2 | 3 | exports.getPopCountList = async function () { 4 | const waifuList = await WaifuRepo.getList() 5 | return waifuList.map(({ waifuId, popCount }) => ({ waifuId, popCount })) 6 | } 7 | -------------------------------------------------------------------------------- /src/server/controller/webClient/WebClientController.js: -------------------------------------------------------------------------------- 1 | const { normalPageHead } = require('./normalPageHead') 2 | const { waifuPopPageHead } = require('./waifuPopPageHead') 3 | 4 | module.exports = { 5 | normalPageHead, 6 | waifuPopPageHead 7 | } 8 | -------------------------------------------------------------------------------- /src/server/controller/webClient/normalPageHead.js: -------------------------------------------------------------------------------- 1 | const { webPageHeadFilePath } = require('~config/webFilePath') 2 | 3 | require('svelte/register') 4 | const PageHead = require(webPageHeadFilePath).default 5 | 6 | exports.normalPageHead = function ({ title = '' } = {}) { 7 | const { head } = PageHead.render({ title }) 8 | return head 9 | } 10 | -------------------------------------------------------------------------------- /src/server/controller/webClient/waifuPopPageHead.js: -------------------------------------------------------------------------------- 1 | const WaifuRepo = require('~entity/waifu/WaifuRepo') 2 | const HttpError = require('~common/error/HttpError') 3 | const { webPageHeadFilePath } = require('~config/webFilePath') 4 | 5 | require('svelte/register') 6 | const PageHead = require(webPageHeadFilePath).default 7 | 8 | exports.waifuPopPageHead = async function ({ waifuUrlId, modeName = 'default' }) { 9 | if (typeof waifuUrlId !== 'string' || waifuUrlId.length < 1) throw new HttpError(301, '/') 10 | if (typeof modeName !== 'string') throw new HttpError(301, '/') 11 | 12 | const waifu = await WaifuRepo.getByUrlId(waifuUrlId) 13 | if (!waifu) throw new HttpError(301, '/') 14 | const mode = waifu.modeConfigList.find((mode) => mode.modeName === modeName) 15 | if (!mode) throw new HttpError(301, `/${waifuUrlId}`) 16 | 17 | const { head } = PageHead.render({ 18 | title: waifu.name, 19 | description: `Click ${waifu.name}`, 20 | image: mode.imgIconUrl 21 | }) 22 | 23 | return head 24 | } 25 | -------------------------------------------------------------------------------- /src/server/expressApp.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const cors = require('cors') 3 | const { ENV, envKeyword } = require('~config/env') 4 | const { webPublicDirPath } = require('~config/webFilePath') 5 | 6 | const { apiRouter } = require('./router/apiRouter') 7 | const { webClientRouter } = require('./router/webClientRouter') 8 | 9 | exports.expressApp = function () { 10 | const app = express() 11 | 12 | let close = false 13 | app.use((req, res, next) => { 14 | if (close) res.setHeader('Connection', 'close') 15 | next() 16 | }) 17 | app.closeConnection = () => { close = true } 18 | 19 | if (ENV === envKeyword.development) app.use(cors()) 20 | app.use(express.urlencoded({ limit: '10mb', extended: true })) 21 | app.use(express.json({ limit: '10mb', extended: true })) 22 | app.use('/api', apiRouter) 23 | app.use(express.static(webPublicDirPath, { maxAge: 10 * 60 * 1000 })) 24 | app.use(webClientRouter) 25 | app.use(function (req, res) { 26 | res.status(404).json({ error: 'unknow path' }) 27 | }) 28 | app.use(errorHandler) 29 | 30 | return app 31 | } 32 | 33 | /** 34 | * @param {Error} err 35 | */ 36 | function errorHandler (err, req, res, next) { 37 | if (err instanceof SyntaxError) { 38 | res.status(400).json({ error: 'invalid json' }) 39 | return next() 40 | } else if (err.message === 'request aborted') { 41 | res.status(400).json() 42 | return next() 43 | } 44 | 45 | let message = `\n!! error at: ${(new Date()).toISOString()}\n` 46 | message += `req.url: ${req.url}\n` 47 | console.error(message) 48 | console.error(err) 49 | 50 | res.status(500).send({ error: 'unknow error' }) 51 | 52 | return next() 53 | } 54 | -------------------------------------------------------------------------------- /src/server/main.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | 3 | const BackgroundRunner = require('~common/utils/BackgroundRunner') 4 | const Mongodb = require('~common/connection/Mongodb') 5 | const { disconnectRedis } = require('~common/connection/redis') 6 | 7 | const { expressApp } = require('./expressApp') 8 | 9 | function main () { 10 | const app = expressApp() 11 | const httpServer = http.createServer(app) 12 | 13 | startServer(httpServer) 14 | 15 | process.on('SIGINT', () => { 16 | console.info('SIGINT signal received') 17 | closeServer(httpServer, app) 18 | }) 19 | } 20 | 21 | async function startServer (SERVER) { 22 | const port = process.env.PORT || 3000 23 | 24 | await Mongodb.connect() 25 | SERVER.listen(port, () => { 26 | console.log('server running') 27 | console.log(`port: ${port}`) 28 | console.log(`environment: ${process.env.NODE_ENV}`) 29 | }) 30 | } 31 | 32 | function closeServer (SERVER, app) { 33 | app.closeConnection() 34 | 35 | console.log(`${new Date().toISOString()} closing http server`) 36 | SERVER.close(async (err) => { 37 | console.log(`${new Date().toISOString()} closed http server`) 38 | if (err) { 39 | console.error(err) 40 | process.exit(1) 41 | } 42 | 43 | console.log(`${new Date().toISOString()} wait BackgroundRunner all done`) 44 | await BackgroundRunner.waitAllDone() 45 | console.log(`${new Date().toISOString()} BackgroundRunner all done`) 46 | 47 | console.log(`${new Date().toISOString()} disconnect`) 48 | await Promise.all([ 49 | Mongodb.disconnect(), 50 | disconnectRedis() 51 | ]) 52 | console.log(`${new Date().toISOString()} disconnect success`) 53 | 54 | console.log(`${new Date().toISOString()} all done, process.exit(0)`) 55 | process.exit(0) 56 | }) 57 | } 58 | 59 | main() 60 | -------------------------------------------------------------------------------- /src/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "~server", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /src/server/router/apiRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | 3 | const { apiSpecRouter } = require('./apiSpecRouter') 4 | const { popRouterV1 } = require('./popRouter') 5 | const { waifuRouterV1 } = require('./waifuRouter') 6 | 7 | const apiRouter = express.Router() 8 | 9 | const v1 = express.Router() 10 | v1.route('/').get((req, res) => res.status(200).json('popwaifu API/V1')) 11 | v1.use('/pop', popRouterV1) 12 | v1.use('/waifu', waifuRouterV1) 13 | 14 | apiRouter.route('/').get(function (req, res) { 15 | res.status(200).json('popwaifu API') 16 | }) 17 | apiRouter.use('/spec', apiSpecRouter) 18 | apiRouter.use('/v1', v1) 19 | 20 | exports.apiRouter = apiRouter 21 | -------------------------------------------------------------------------------- /src/server/router/apiSpecRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const swaggerUi = require('swagger-ui-express') 3 | const SwaggerParser = require('@apidevtools/swagger-parser') 4 | 5 | const { serverApiSpecMainPath } = require('~config/apiSpecPath') 6 | const { consoleUnexpectedError } = require('~common/error/consoleUnexpectedError') 7 | 8 | const apiSpecRouter = express.Router() 9 | 10 | const htmlPromise = genHtml() 11 | async function genHtml () { 12 | const swaggerDoc = await SwaggerParser.bundle(serverApiSpecMainPath) 13 | return swaggerUi.generateHTML(swaggerDoc, { customSiteTitle: 'popwaifu API spec' }) 14 | } 15 | 16 | apiSpecRouter.use((req, res, next) => { 17 | res.setHeader('Cache-Control', 'public, max-age=900') 18 | return next() 19 | }) 20 | 21 | apiSpecRouter.use(swaggerUi.serve) 22 | 23 | apiSpecRouter.get('/', async (req, res) => { 24 | try { 25 | const page = await htmlPromise 26 | return res.status(200).send(page) 27 | } catch (err) { 28 | consoleUnexpectedError(err) 29 | return res.status(500).json({ error: 'unknow swagger error' }) 30 | } 31 | }) 32 | 33 | exports.apiSpecRouter = apiSpecRouter 34 | -------------------------------------------------------------------------------- /src/server/router/getIp.js: -------------------------------------------------------------------------------- 1 | const ipaddr = require('ipaddr.js') 2 | const { ipv4Trust, ipv6Trust } = require('~config/ipTrustConfig') 3 | 4 | /** 5 | * @typedef {import('@types/express').Request} Request 6 | */ 7 | 8 | /** 9 | * @param {Request} req 10 | */ 11 | exports.getIp = function (req) { 12 | const forwarded = req.headers['x-forwarded-for'] 13 | if (typeof forwarded !== 'string') return req.socket.remoteAddress 14 | 15 | const ips = forwarded.split(/ *, */) 16 | for (let i = ips.length - 1; i >= 0; i -= 1) { 17 | const ip = ips[i] 18 | if (!isTrust(ip)) return ip 19 | } 20 | 21 | return req.socket.remoteAddress 22 | } 23 | 24 | const ipv4CidrTrustList = ipv4Trust.map((cidr) => ipaddr.parseCIDR(cidr)) 25 | const ipv6CidrTrustList = ipv6Trust.map((cidr) => ipaddr.parseCIDR(cidr)) 26 | function isTrust (ip) { 27 | return ipaddr.IPv4.isIPv4(ip) 28 | ? isMatchCidr(ip, ipv4CidrTrustList) 29 | : isMatchCidr(ip, ipv6CidrTrustList) 30 | } 31 | 32 | function isMatchCidr (ip, cidrList) { 33 | const addr = ipaddr.parse(ip) 34 | for (const cidr of cidrList) { 35 | if (addr.match(cidr)) return true 36 | } 37 | return false 38 | } 39 | -------------------------------------------------------------------------------- /src/server/router/getReqHandleFunc.js: -------------------------------------------------------------------------------- 1 | const HttpError = require('~common/error/HttpError') 2 | const { consoleUnexpectedError } = require('~common/error/consoleUnexpectedError') 3 | 4 | /** 5 | * @typedef {import('@types/express').Request} Request 6 | * @typedef {import('@types/express').Response} Response 7 | */ 8 | /** 9 | * @callback ReqCallback 10 | * @param {Request} req 11 | */ 12 | /** 13 | * @typedef {Object} Options 14 | * @property {String} [cacheControl] 15 | * @property {Number} [successStatusCode] 16 | */ 17 | 18 | /** 19 | * @param {ReqCallback} callback 20 | * @param {Options} [options] 21 | */ 22 | exports.getReqHandleFunc = function (callback, options = {}) { 23 | return function (req, res, next) { 24 | return tryHandleReq(req, res, next, callback, options) 25 | } 26 | } 27 | 28 | /** 29 | * @param {Request} req 30 | * @param {Response} res 31 | * @param {Options} options 32 | */ 33 | async function tryHandleReq (req, res, next, callback, options) { 34 | try { 35 | const result = await callback(req) 36 | if (options.cacheControl) res.setHeader('Cache-Control', options.cacheControl) 37 | 38 | const statusCode = options.successStatusCode || 200 39 | return res.status(statusCode).json(result) 40 | } catch (err) { 41 | if (err instanceof HttpError) { 42 | return res.status(err.statusCode).json({ error: err.message }) 43 | } else { 44 | consoleUnexpectedError(err) 45 | return res.status(500).json({ error: 'server unexpected error' }) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/server/router/popRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | 3 | const PopController = require('../controller/pop/PopController') 4 | const { getIp } = require('./getIp') 5 | const { getReqHandleFunc } = require('./getReqHandleFunc') 6 | 7 | const popRouterV1 = express.Router() 8 | 9 | popRouterV1.route('/record') 10 | .post(getReqHandleFunc((req) => { 11 | return PopController.recordPop({ 12 | ip: getIp(req), 13 | waifuPopObj: req.body.waifuPopObj 14 | }) 15 | }, { successStatusCode: 202 })) 16 | 17 | module.exports = { 18 | popRouterV1 19 | } 20 | -------------------------------------------------------------------------------- /src/server/router/waifuRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | 3 | const WaifuController = require('../controller/waifu/WaifuController') 4 | const { getReqHandleFunc } = require('./getReqHandleFunc') 5 | 6 | const waifuRouterV1 = express.Router() 7 | 8 | waifuRouterV1.route('/list') 9 | .get(getReqHandleFunc( 10 | (req) => WaifuController.getList(), 11 | { cacheControl: 'public, max-age=5' } 12 | )) 13 | 14 | waifuRouterV1.route('/list/popcount') 15 | .get(getReqHandleFunc( 16 | (req) => WaifuController.getPopCountList(), 17 | { cacheControl: 'public, max-age=5' } 18 | )) 19 | 20 | module.exports = { 21 | waifuRouterV1 22 | } 23 | -------------------------------------------------------------------------------- /src/server/router/webClientRouter.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const express = require('express') 3 | 4 | const HttpError = require('~common/error/HttpError') 5 | const { consoleUnexpectedError } = require('~common/error/consoleUnexpectedError') 6 | const { webIndexFilePath } = require('~config/webFilePath') 7 | 8 | const WebClientController = require('../controller/webClient/WebClientController') 9 | 10 | /** 11 | * @typedef {import('@types/express').Request} Request 12 | * @typedef {import('@types/express').Response} Response 13 | */ 14 | /** 15 | * @callback ReqCallback 16 | * @param {Request} req 17 | */ 18 | 19 | const webClientRouter = express.Router() 20 | const indexFileStr = fs.readFileSync(webIndexFilePath, 'utf8') 21 | const replaceRegex = /(?:.|\n)*/m 22 | 23 | webClientRouter.get('/example', getPageHandleFunc((req) => { 24 | return WebClientController.normalPageHead({ title: 'example' }) 25 | })) 26 | 27 | webClientRouter.get('/:urlId/:mode', getPageHandleFunc((req) => { 28 | return WebClientController.waifuPopPageHead({ 29 | waifuUrlId: req.params.urlId, 30 | modeName: req.params.mode 31 | }) 32 | })) 33 | 34 | webClientRouter.get('/:urlId', getPageHandleFunc((req) => { 35 | return WebClientController.waifuPopPageHead({ waifuUrlId: req.params.urlId }) 36 | })) 37 | 38 | webClientRouter.get('*', getPageHandleFunc((req) => { 39 | return WebClientController.normalPageHead() 40 | })) 41 | 42 | exports.webClientRouter = webClientRouter 43 | 44 | /** 45 | * @param {ReqCallback} getHeadFunc 46 | */ 47 | function getPageHandleFunc (getHeadFunc) { 48 | return function (req, res) { 49 | return tryHandle(req, res, getHeadFunc) 50 | } 51 | } 52 | 53 | /** 54 | * @param {Request} req 55 | * @param {Response} res 56 | */ 57 | async function tryHandle (req, res, getHeadFunc) { 58 | try { 59 | const head = await getHeadFunc(req) 60 | const indexWithHead = indexFileStr.replace(replaceRegex, head) 61 | res.setHeader('Cache-Control', 'public, max-age=600') 62 | return res.status(200).send(indexWithHead) 63 | } catch (err) { 64 | if (err instanceof HttpError && err.statusCode === 301) { 65 | return res.redirect(err.message || '/') 66 | } else { 67 | consoleUnexpectedError(err) 68 | return res.redirect('/') 69 | } 70 | } 71 | } 72 | --------------------------------------------------------------------------------