├── .browserslistrc ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .prettierrc.json ├── README.md ├── babel.config.js ├── icons ├── 256x256.png ├── 512x512.png ├── icon.icns ├── icon.ico └── icon.svg ├── jsconfig.json ├── package-lock.json ├── package.json ├── prisma ├── main.db └── schema.prisma ├── src ├── assets │ └── fonts │ │ ├── Saitamaar.ttf │ │ ├── Saitamaar.woff │ │ ├── Saitamaar.woff2 │ │ └── font.css ├── background.js ├── db-manage.js ├── main-config.js ├── main.js └── renderer │ ├── app.vue │ ├── images │ └── cover.png │ ├── utils │ ├── connector.js │ ├── data.js │ └── renderer-utils.js │ └── views │ ├── article.vue │ ├── copyright.vue │ ├── empty.vue │ ├── favorite.vue │ ├── history.vue │ ├── manage.vue │ ├── menu.vue │ └── sub-components │ ├── article-table.vue │ ├── pub-article.vue │ ├── random-articles.vue │ ├── recommended-articles.vue │ └── show-article.vue ├── tools ├── androidTransfer.js ├── base64exporter.js ├── base64modifier.js ├── contentCleaner.js ├── copyrightGenerator.js ├── coverSetter.js ├── creatorChecker.js ├── dataAnalyser.js ├── dataCleaner.js ├── dbMerger.js ├── duplicateChecker.js ├── metaTransfer.js ├── recFixer.js ├── result │ ├── .keep │ ├── paint_R15.py │ ├── paint_R18.py │ ├── plot_count_all.py │ ├── plot_count_creators.py │ ├── plot_count_delta.py │ ├── plot_count_tags.py │ ├── plot_len_all.py │ ├── plot_len_creators.py │ ├── plot_len_delta.py │ └── plot_len_tags.py ├── signGenerator.js ├── sourceChecker.js ├── tagAnalyser.js └── translatorReward.js ├── vue.config.js └── web-types.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | extends: [ 6 | 'plugin:vue/essential', 7 | 'eslint:recommended', 8 | 'plugin:prettier/recommended' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint', 12 | }, 13 | root: true, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 17 | 'prettier/prettier': [ 18 | 'warn', 19 | { 20 | endOfLine: 'auto', 21 | semi: true, 22 | singleQuote: true, 23 | trailingComma: 'all', 24 | }, 25 | ], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.db filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: umalib-build-and-release 5 | 6 | on: 7 | push: 8 | tags: 9 | - "v*" 10 | 11 | jobs: 12 | build-on-windows: 13 | runs-on: windows-2019 14 | strategy: 15 | matrix: 16 | node-version: [ 13.x ] 17 | permissions: 18 | contents: write 19 | steps: 20 | - name: Checkout source 21 | uses: actions/checkout@v4 22 | with: 23 | lfs: 'true' 24 | persist-credentials: false 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'npm' 30 | - name: Manually install dependencies 31 | run: npm install electron-builder@22.10.5 32 | - name: Install dependencies 33 | run: npm install 34 | - name: Build 35 | run: npm run electron:build 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | - name: Release 40 | uses: softprops/action-gh-release@v1 41 | with: 42 | tag_name: ${{ github.ref_name }} 43 | name: Uma Library Desktop ${{ github.ref_name }} 44 | body: built by Github Actions 45 | draft: false 46 | prerelease: false 47 | files: ./dist_electron/umalib-win64-${{ github.ref_name }}.7z 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | build-on-macos: 51 | runs-on: macos-latest 52 | strategy: 53 | matrix: 54 | node-version: [ 14.x ] 55 | permissions: 56 | contents: write 57 | steps: 58 | - name: Checkout source 59 | uses: actions/checkout@v4 60 | with: 61 | lfs: 'true' 62 | persist-credentials: false 63 | - name: Use Node.js ${{ matrix.node-version }} 64 | uses: actions/setup-node@v4 65 | with: 66 | node-version: ${{ matrix.node-version }} 67 | cache: 'npm' 68 | - name: Install dependencies 69 | run: npm install 70 | - name: Build 71 | run: npm run electron:build 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | PYTHON_PATH: /usr/local/bin/python 76 | - name: Release 77 | uses: softprops/action-gh-release@v1 78 | with: 79 | tag_name: ${{ github.ref_name }} 80 | name: Uma Library Desktop ${{ github.ref_name }} 81 | files: ./dist_electron/umalib-mac-${{ github.ref_name }}.dmg 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | #Electron-builder output 25 | /dist_electron 26 | prisma/migrations 27 | /tools/config.js 28 | /tools/remover.js 29 | /tools/input.js 30 | /tools/result/ 31 | /data 32 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uma Library Desktop 2 | 3 | A library of doujin articles of _Umamusume: Pretty Derby_ 4 | 5 | ## Project setup 6 | ``` 7 | npm install 8 | ``` 9 | 10 | ### Compiles and hot-reloads for development 11 | ``` 12 | npm run electron:serve 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | ``` 17 | npm run electron:build 18 | ``` 19 | 20 | ### Lints and fixes files 21 | ``` 22 | npm run postinstall 23 | ``` 24 | 25 | ## Copyright 26 | 27 | Developed based on [vue-electron-prisma-test](https://github.com/clementvp/vue-electron-prisma-test) 28 | 29 | ## Support 30 | 31 | Publishing post (Chinese): https://bbs.nga.cn/read.php?tid=32535194 -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umalib/UmaLibDesktop/f6e58b5e6106b8490f3fd01b1ffc9e16680866b2/icons/256x256.png -------------------------------------------------------------------------------- /icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umalib/UmaLibDesktop/f6e58b5e6106b8490f3fd01b1ffc9e16680866b2/icons/512x512.png -------------------------------------------------------------------------------- /icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umalib/UmaLibDesktop/f6e58b5e6106b8490f3fd01b1ffc9e16680866b2/icons/icon.icns -------------------------------------------------------------------------------- /icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umalib/UmaLibDesktop/f6e58b5e6106b8490f3fd01b1ffc9e16680866b2/icons/icon.ico -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": [ 5 | "src/*" 6 | ] 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "WindyWhisper ", 3 | "dependencies": { 4 | "@prisma/client": "^3.15.2", 5 | "axios": "^0.27.2", 6 | "compressing": "^1.10.0", 7 | "element-ui": "^2.15.14", 8 | "jshashes": "^1.0.8", 9 | "log4js": "^6.9.1", 10 | "opencc-js": "^1.0.4", 11 | "prisma": "^3.15.2", 12 | "vue": "^2.7.16", 13 | "vue-quill-editor": "^3.0.6", 14 | "vue-router": "^3.6.5" 15 | }, 16 | "description": "A library of doujin articles of Umamusume: Pretty Derby", 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "^4.5.19", 19 | "@vue/cli-plugin-eslint": "^4.5.19", 20 | "@vue/cli-plugin-router": "^4.5.19", 21 | "@vue/cli-plugin-vuex": "^4.5.19", 22 | "@vue/cli-service": "^4.5.19", 23 | "@vue/eslint-config-prettier": "^6.0.0", 24 | "babel-eslint": "^10.1.0", 25 | "copy-webpack-plugin": "^6.4.1", 26 | "core-js": "^3.36.1", 27 | "electron": "^19.1.9", 28 | "electron-builder": "^22.10.5", 29 | "electron-devtools-installer": "^3.1.0", 30 | "electron-store": "^8.2.0", 31 | "element-theme": "^2.0.1", 32 | "element-theme-chalk": "^2.15.14", 33 | "eslint": "^6.7.2", 34 | "eslint-plugin-prettier": "^3.1.3", 35 | "eslint-plugin-vue": "^6.2.2", 36 | "node-sass": "^4.14.1", 37 | "prettier": "^1.19.1", 38 | "sass-loader": "^8.0.2", 39 | "sm-crypto": "^0.3.13", 40 | "vue-cli-plugin-electron-builder": "~2.0.0-rc.6", 41 | "vue-template-compiler": "^2.7.16", 42 | "webpack": "^4.47.0" 43 | }, 44 | "electronVersion": "19.1.9", 45 | "main": "background.js", 46 | "name": "uma-library-desktop", 47 | "private": true, 48 | "scripts": { 49 | "build:local": "vue-cli-service electron:build --dir", 50 | "electron:build": "vue-cli-service electron:build", 51 | "electron:serve": "vue-cli-service electron:serve", 52 | "lint": "vue-cli-service lint", 53 | "postinstall": "electron-builder install-app-deps", 54 | "postuninstall": "electron-builder install-app-deps", 55 | "sqlite": "prisma introspect && prisma studio" 56 | }, 57 | "version": "3.0.2-Final", 58 | "web-types": "./web-types.json" 59 | } 60 | -------------------------------------------------------------------------------- /prisma/main.db: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e0cef2348d6c73247b30a6079a9e8d19bc0d20b3aeff1728d0644061f1cdf0f4 3 | size 32768 4 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "sqlite" 7 | url = "file:./main.db" 8 | } 9 | 10 | model article { 11 | id Int @id @default(autoincrement()) 12 | name String? 13 | note String? 14 | content String? 15 | author String? 16 | translator String? 17 | uploadTime Int? 18 | source String? 19 | taggedList tagged[] 20 | } 21 | 22 | model tag { 23 | id Int @id @default(autoincrement()) 24 | name String? 25 | type Int? 26 | cover String? 27 | description String? 28 | taggedList tagged[] 29 | } 30 | 31 | model tagged { 32 | id Int @id @default(autoincrement()) 33 | artId Int 34 | tagId Int 35 | article article @relation(fields: [artId], references: [id], onDelete: NoAction, onUpdate: NoAction) 36 | tag tag @relation(fields: [tagId], references: [id], onDelete: NoAction, onUpdate: NoAction) 37 | } 38 | 39 | model creator { 40 | id Int @id @default(autoincrement()) 41 | names String? 42 | } 43 | 44 | model rec { 45 | id Int @id @default(autoincrement()) 46 | refId Int 47 | title String 48 | others String? @default("") 49 | type Int? @default(0) 50 | r Int? @default(0) 51 | name String 52 | reason String 53 | } 54 | 55 | model dict { 56 | id Int @id @default(autoincrement()) 57 | class String? 58 | desc String 59 | key String? 60 | refId Int? 61 | related String? 62 | relatedId Int? 63 | } 64 | -------------------------------------------------------------------------------- /src/assets/fonts/Saitamaar.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umalib/UmaLibDesktop/f6e58b5e6106b8490f3fd01b1ffc9e16680866b2/src/assets/fonts/Saitamaar.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Saitamaar.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umalib/UmaLibDesktop/f6e58b5e6106b8490f3fd01b1ffc9e16680866b2/src/assets/fonts/Saitamaar.woff -------------------------------------------------------------------------------- /src/assets/fonts/Saitamaar.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umalib/UmaLibDesktop/f6e58b5e6106b8490f3fd01b1ffc9e16680866b2/src/assets/fonts/Saitamaar.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Saitamaar'; 3 | src: url('./Saitamaar.woff2') format('woff2'), 4 | url('./Saitamaar.woff') format('woff'), 5 | url('./Saitamaar.ttf') format('ttf'); 6 | font-display: swap; 7 | } -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let { app } = require('electron'); 4 | const { 5 | BrowserWindow, 6 | dialog, 7 | ipcMain, 8 | Menu, 9 | protocol, 10 | remote, 11 | session, 12 | shell, 13 | } = require('electron'); 14 | const { createProtocol } = require('vue-cli-plugin-electron-builder/lib'); 15 | const { 16 | existsSync, 17 | readdirSync, 18 | readFileSync, 19 | writeFileSync, 20 | copyFileSync, 21 | } = require('fs'); 22 | const MD5 = new (require('jshashes').MD5)(); 23 | const log4js = require('log4js'); 24 | const { homedir } = require('os'); 25 | const { resolve, join } = require('path'); 26 | 27 | const dbManage = require('@/db-manage'); 28 | const { titles, themes } = require('@/main-config'); 29 | 30 | const isDevelopment = process.env.NODE_ENV !== 'production'; 31 | 32 | if (!app) { 33 | app = remote.app; 34 | } 35 | 36 | log4js.configure({ 37 | appenders: { 38 | console: { type: 'console' }, 39 | file: { 40 | filename: join(app.getPath('userData'), './logs/main.log'), 41 | keepFileExt: true, 42 | pattern: 'yyMMdd', 43 | type: 'dateFile', 44 | }, 45 | }, 46 | categories: { 47 | default: { appenders: ['console', 'file'], level: 'info' }, 48 | }, 49 | }); 50 | const logger = log4js.getLogger('background'); 51 | const dbLogger = log4js.getLogger('db-manage'); 52 | const appLogger = log4js.getLogger('app'); 53 | const userDbPath = join(app.getPath('userData'), './main.db'); 54 | if (isDevelopment) { 55 | logger.level = 'debug'; 56 | dbLogger.level = 'debug'; 57 | } 58 | 59 | if (!existsSync(userDbPath)) { 60 | copyFileSync( 61 | isDevelopment 62 | ? join(resolve('./prisma/main.db')) 63 | : join(process.resourcesPath, './prisma/main.db'), 64 | userDbPath, 65 | ); 66 | logger.info(`copy default database to user data folder: ${userDbPath}`); 67 | } 68 | dbManage.config(dbLogger, app.getPath('userData'), userDbPath); 69 | 70 | const configStore = new (require('electron-store'))(); 71 | logger.debug(`use config path: ${configStore.path}`); 72 | 73 | function chooseTitles() { 74 | let rand = Math.random(); 75 | rand -= 0.04; 76 | if (rand < 0) { 77 | return titles.mejiro; 78 | } 79 | rand -= 0.04; 80 | if (rand < 0) { 81 | return titles.agnes; 82 | } 83 | rand -= 0.04; 84 | if (rand < 0) { 85 | return titles.satono; 86 | } 87 | rand -= 0.04; 88 | if (rand < 0) { 89 | return titles.traincen; 90 | } 91 | rand -= 0.04; 92 | if (rand < 0) { 93 | return titles.oguri; 94 | } 95 | rand -= 0.01; 96 | if (rand < 0) { 97 | return titles.cemetery; 98 | } 99 | return titles.origin; 100 | } 101 | 102 | const storeEvents = { 103 | pathConf: MD5.hex(dbManage.getPath()), 104 | getOrCreateConfig() { 105 | const defaultConf = { 106 | favorites: [], 107 | password: '', 108 | }; 109 | let ret = configStore.get(this.pathConf); 110 | if (!ret) { 111 | ret = configStore.get(dbManage.getPath()); 112 | if (!ret) { 113 | configStore.set(this.pathConf, defaultConf); 114 | return defaultConf; 115 | } 116 | configStore.set(this.pathConf, ret); 117 | } 118 | return ret; 119 | }, 120 | resetConfig() { 121 | this.pathConf = MD5.hex(dbManage.getPath()); 122 | this.saveMeFlag = -2; 123 | }, 124 | 125 | checkVersion() { 126 | return { 127 | app: app.getVersion(), 128 | db: configStore.get('db-version'), 129 | dbUpdate: this.getCheckDbUpdate(), 130 | }; 131 | }, 132 | setDbVersion(version) { 133 | configStore.set('db-version', version); 134 | this.resetConfig(); 135 | dbManage.cleanBackupDb(); 136 | }, 137 | 138 | getCheckDbUpdate() { 139 | return configStore.get('db-update'); 140 | }, 141 | setCheckDbUpdate(dbUpdate) { 142 | configStore.set('db-update', dbUpdate); 143 | return dbUpdate; 144 | }, 145 | 146 | getDefaultFullScreen() { 147 | return configStore.get('full-screen'); 148 | }, 149 | setDefaultFullScreen(isFullScreen) { 150 | configStore.set('full-screen', isFullScreen); 151 | return isFullScreen; 152 | }, 153 | 154 | backgroundColor: undefined, 155 | getBackgroundColor() { 156 | if (!this.backgroundColor) { 157 | this.backgroundColor = configStore.get('background-color'); 158 | } 159 | return this.backgroundColor; 160 | }, 161 | setBackgroundColor(color) { 162 | this.backgroundColor = color; 163 | configStore.set('background-color', this.backgroundColor); 164 | return color; 165 | }, 166 | 167 | saveMeFlag: -2, 168 | password: '', 169 | getPwd() { 170 | return this.password; 171 | }, 172 | isSafe(isSet) { 173 | this.saveMeFlag = -3; 174 | if (isSet) { 175 | const config = this.getOrCreateConfig(); 176 | config.password = MD5.hex(this.password); 177 | configStore.set(this.pathConf, config); 178 | } 179 | }, 180 | async saveMe() { 181 | if (this.saveMeFlag === -2) { 182 | this.password = await dbManage.getPassword(); 183 | if (this.password) { 184 | this.password = MD5.hex(this.password); 185 | this.password = this.password.substring(this.password.length - 8); 186 | } 187 | if ( 188 | !this.password || 189 | MD5.hex(this.password) === this.getOrCreateConfig().password 190 | ) { 191 | this.saveMeFlag = -1; 192 | } else { 193 | this.saveMeFlag = await dbManage.checkR18(); 194 | } 195 | logger.debug(`R18 id=${this.saveMeFlag}, pwd=${this.password}`); 196 | } 197 | return this.saveMeFlag; 198 | }, 199 | 200 | addFavorite(id) { 201 | const list = this.getFavorites(); 202 | list.push(id); 203 | this.setFavorites(list); 204 | return list; 205 | }, 206 | removeFavorite(id) { 207 | const list = this.getFavorites().filter(x => x !== id); 208 | this.setFavorites(list); 209 | return list; 210 | }, 211 | getFavorites() { 212 | return this.getOrCreateConfig().favorites; 213 | }, 214 | setFavorites(favorites) { 215 | const ret = configStore.get(this.pathConf); 216 | ret.favorites = favorites.filter((v, i, l) => l.indexOf(v) === i); 217 | configStore.set(this.pathConf, ret); 218 | }, 219 | async importFavorites() { 220 | const paths = dialog['showOpenDialogSync']({ 221 | filters: [{ name: 'json', extensions: ['json'] }], 222 | multiSelections: false, 223 | openDirectory: false, 224 | }); 225 | if (paths && paths.length) { 226 | try { 227 | const favList = this.getFavorites().concat( 228 | await dbManage.getIdsByFav( 229 | JSON.parse(readFileSync(paths[0], 'utf-8')), 230 | ), 231 | ); 232 | this.setFavorites(favList); 233 | return this.getFavorites(); 234 | } catch (ignored) { 235 | return true; 236 | } 237 | } 238 | }, 239 | async exportFavorites() { 240 | const path = dialog['showSaveDialogSync']({ 241 | title: '导出收藏夹配置到……', 242 | defaultPath: `./${this.pathConf}.json`, 243 | }); 244 | if (path) { 245 | writeFileSync( 246 | path, 247 | JSON.stringify(await dbManage.listAllFav(this.getFavorites())), 248 | ); 249 | } 250 | return path; 251 | }, 252 | 253 | titles: chooseTitles(), 254 | getTitles() { 255 | return this.titles; 256 | }, 257 | 258 | log(args) { 259 | appLogger[args.level](args.info); 260 | }, 261 | 262 | keyword: '', 263 | }; 264 | 265 | if (storeEvents.getCheckDbUpdate() === undefined) { 266 | logger.info('write default check-db-update into config'); 267 | storeEvents.setCheckDbUpdate(true); 268 | } 269 | 270 | if (storeEvents.getDefaultFullScreen() === undefined) { 271 | logger.info('write default full-screen into config'); 272 | storeEvents.setDefaultFullScreen(true); 273 | } 274 | 275 | if (!storeEvents.getBackgroundColor()) { 276 | logger.info('write default background-color into config'); 277 | storeEvents.setBackgroundColor(themes[0].color); 278 | } 279 | 280 | // Scheme must be registered before the app is ready 281 | protocol.registerSchemesAsPrivileged([ 282 | { scheme: 'app', privileges: { secure: true, standard: true } }, 283 | ]); 284 | 285 | async function createWindow() { 286 | // Create the browser window. 287 | const mainWindow = new BrowserWindow({ 288 | width: 1600, 289 | height: 900, 290 | minWidth: 720, 291 | minHeight: 480, 292 | show: false, 293 | title: '赛马娘同人集中楼大书库', 294 | webPreferences: { 295 | contextIsolation: false, 296 | // Use pluginOptions.nodeIntegration, leave this alone 297 | // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info 298 | nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION, 299 | }, 300 | }); 301 | 302 | mainWindow.webContents.session.webRequest.onBeforeSendHeaders( 303 | (details, callback) => { 304 | callback({ requestHeaders: { Origin: '*', ...details.requestHeaders } }); 305 | }, 306 | ); 307 | 308 | mainWindow.webContents.session.webRequest.onHeadersReceived( 309 | (details, callback) => { 310 | const headerKey = 'Access-Control-Allow-Origin'; 311 | if ( 312 | !details.responseHeaders[headerKey] && 313 | !details.responseHeaders[headerKey.toLowerCase()] 314 | ) { 315 | details.responseHeaders[headerKey.toLowerCase()] = ['*']; 316 | } 317 | callback(details); 318 | }, 319 | ); 320 | 321 | function setRendererBackgroundColor(color) { 322 | color && storeEvents.setBackgroundColor(color); 323 | mainWindow.webContents.send('colorEvent', storeEvents.getBackgroundColor()); 324 | } 325 | 326 | logger.info('clean old listeners'); 327 | ipcMain.removeAllListeners(); 328 | 329 | ipcMain.on('artChannel', async (_, msg) => { 330 | let result = undefined; 331 | const start = new Date().getTime(); 332 | try { 333 | if (dbManage[msg.action]) { 334 | logger.debug(`dbManage.${msg.action}(${JSON.stringify(msg.args)})`); 335 | result = await dbManage[msg.action](msg.args); 336 | if (msg.action === 'changeDb') { 337 | logger.info(`change db to ${msg.args}`); 338 | storeEvents.resetConfig(); 339 | } 340 | } else { 341 | result = await storeEvents[msg.action](msg.args); 342 | logger.debug(`storeEvents.${msg.action}: ${JSON.stringify(result)}`); 343 | } 344 | } catch (e) { 345 | logger.error(e.toString()); 346 | } 347 | logger.info( 348 | `${dbManage[msg.action] ? 'dbManage' : 'storeEvents'}.${ 349 | msg.action 350 | }: ${new Date().getTime() - start} ms`, 351 | ); 352 | mainWindow.webContents.send('artChannel', { 353 | id: msg.id, 354 | data: result, 355 | }); 356 | }); 357 | 358 | ipcMain.on('colorEvent', () => setRendererBackgroundColor()); 359 | 360 | ipcMain.on('findInPage', (_, keyword) => { 361 | if (keyword) { 362 | mainWindow.webContents.findInPage(keyword, { 363 | findNext: keyword !== storeEvents.keyword, 364 | }); 365 | } else { 366 | mainWindow.webContents.stopFindInPage('clearSelection'); 367 | } 368 | }); 369 | 370 | logger.info('register new listeners'); 371 | 372 | if (process.env.WEBPACK_DEV_SERVER_URL) { 373 | // Load the url of the dev server if in development mode 374 | await mainWindow.loadURL(process.env.WEBPACK_DEV_SERVER_URL); 375 | if (!process.env.IS_TEST) { 376 | mainWindow.webContents.openDevTools(); 377 | } 378 | } else { 379 | createProtocol('app'); 380 | // Load the index.html when not in development 381 | await mainWindow.loadURL('app://./index.html'); 382 | } 383 | 384 | const template = []; 385 | if (process.platform === 'darwin') { 386 | template.push({ 387 | role: 'appMenu', 388 | }); 389 | } 390 | template.push({ 391 | label: '功能', 392 | submenu: [ 393 | { 394 | label: storeEvents.titles.list, 395 | sublabel: '文章列表', 396 | toolTip: '文章列表', 397 | accelerator: 'CmdOrCtrl+1', 398 | click() { 399 | mainWindow.webContents.send('menuEvent', '/list'); 400 | }, 401 | }, 402 | { 403 | label: storeEvents.titles.menu, 404 | sublabel: '长篇/合集目录', 405 | toolTip: '长篇/合集目录', 406 | accelerator: 'CmdOrCtrl+2', 407 | click() { 408 | mainWindow.webContents.send('menuEvent', '/menu/m'); 409 | }, 410 | }, 411 | { 412 | label: storeEvents.titles.favorite, 413 | sublabel: '收藏夹', 414 | toolTip: '收藏夹', 415 | accelerator: 'CmdOrCtrl+3', 416 | click() { 417 | mainWindow.webContents.send('menuEvent', '/favorites'); 418 | }, 419 | }, 420 | { 421 | label: storeEvents.titles.history, 422 | sublabel: '阅读历史', 423 | toolTip: '阅读历史', 424 | accelerator: 'CmdOrCtrl+4', 425 | click() { 426 | mainWindow.webContents.send('menuEvent', '/history'); 427 | }, 428 | }, 429 | { 430 | label: storeEvents.titles.manage, 431 | sublabel: '管理', 432 | toolTip: '管理', 433 | accelerator: 'CmdOrCtrl+5', 434 | click() { 435 | mainWindow.webContents.send('menuEvent', '/manage'); 436 | }, 437 | }, 438 | { 439 | label: storeEvents.titles.copyright, 440 | sublabel: '鸣谢', 441 | toolTip: '鸣谢', 442 | accelerator: 'CmdOrCtrl+6', 443 | click() { 444 | mainWindow.webContents.send('menuEvent', '/copyright'); 445 | }, 446 | }, 447 | { type: 'separator' }, 448 | { 449 | label: '选择数据库', 450 | async click() { 451 | const paths = dialog['showOpenDialogSync']({ 452 | filters: [{ name: 'db', extensions: ['db'] }], 453 | multiSelections: false, 454 | openDirectory: false, 455 | }); 456 | if (paths && paths.length) { 457 | logger.info(`dbManage.changeDb("${paths[0]}")`); 458 | const result = await dbManage.changeDb(paths[0]); 459 | storeEvents.resetConfig(); 460 | mainWindow.webContents.send('refreshPage', result); 461 | } 462 | }, 463 | }, 464 | { 465 | label: '切换到内置数据库', 466 | async click() { 467 | logger.info('dbManage.resetDb()'); 468 | await dbManage.resetDb(); 469 | storeEvents.resetConfig(); 470 | mainWindow.webContents.send('refreshPage', { 471 | current: userDbPath, 472 | isEmbedded: true, 473 | }); 474 | }, 475 | }, 476 | { 477 | label: '重载数据库', 478 | sublabel: '从云端拉取内置数据库', 479 | toolTip: '从云端拉取内置数据库', 480 | click() { 481 | mainWindow.webContents.send('getOnlineDb', ''); 482 | }, 483 | }, 484 | { 485 | label: '安装本地数据库', 486 | sublabel: '从本地安装内置数据库', 487 | toolTip: '从本地安装内置数据库', 488 | async click() { 489 | const paths = dialog['showOpenDialogSync']({ 490 | filters: [{ name: 'zip', extensions: ['zip'] }], 491 | multiSelections: false, 492 | openDirectory: false, 493 | }); 494 | if (paths && paths.length) { 495 | try { 496 | logger.info(`dbManage.changeDb("${paths[0]}")`); 497 | const result = await dbManage.saveOnlineDb(paths[0]); 498 | await dbManage.checkR18(); 499 | storeEvents.setDbVersion(result.dbVersion); 500 | mainWindow.webContents.send('refreshPage', result); 501 | } catch (_) { 502 | logger.error('zip is corrupted!'); 503 | await dbManage.rollbackDb(); 504 | mainWindow.webContents.send('refreshPage', { 505 | current: userDbPath, 506 | isEmbedded: true, 507 | }); 508 | } 509 | } 510 | }, 511 | }, 512 | { 513 | label: '启动时检查数据库', 514 | sublabel: '启动时是否检查数据库更新', 515 | toolTip: '启动时是否检查数据库更新', 516 | type: 'checkbox', 517 | checked: storeEvents.getCheckDbUpdate(), 518 | click(event) { 519 | storeEvents.setCheckDbUpdate(event.checked); 520 | }, 521 | }, 522 | { type: 'separator' }, 523 | { label: '退出', role: 'quit' }, 524 | ], 525 | }); 526 | template.push({ 527 | label: '编辑', 528 | submenu: [ 529 | { 530 | label: '撤销', 531 | role: 'undo', 532 | }, 533 | { 534 | label: '重复', 535 | role: 'redo', 536 | }, 537 | { type: 'separator' }, 538 | { label: '剪切', role: 'cut' }, 539 | { label: '复制', role: 'copy' }, 540 | { label: '粘贴', role: 'paste' }, 541 | { label: '符合格式粘贴', role: 'pasteAndMatchStyle' }, 542 | { label: '全选', role: 'selectAll' }, 543 | { 544 | label: '查找', 545 | accelerator: 'CmdOrCtrl+F', 546 | click() { 547 | mainWindow.webContents.send('findInPage', ''); 548 | }, 549 | }, 550 | ], 551 | }); 552 | template.push({ 553 | label: '界面', 554 | submenu: [ 555 | { 556 | label: '主题', 557 | submenu: themes.map(c => { 558 | return { 559 | label: c.label, 560 | type: 'radio', 561 | checked: c.color === storeEvents.getBackgroundColor(), 562 | click() { 563 | setRendererBackgroundColor(c.color); 564 | }, 565 | }; 566 | }), 567 | }, 568 | { label: '刷新', role: 'reload' }, 569 | { type: 'separator' }, 570 | { label: '全屏', role: 'togglefullscreen' }, 571 | { label: '最小化', role: 'minimize' }, 572 | { label: '重置', role: 'resetzoom' }, 573 | { 574 | label: '启动时最大化', 575 | sublabel: '是/否', 576 | toolTip: '是/否', 577 | type: 'checkbox', 578 | checked: storeEvents.getDefaultFullScreen(), 579 | click(event) { 580 | storeEvents.setDefaultFullScreen(event.checked); 581 | }, 582 | }, 583 | ], 584 | }); 585 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)); 586 | setRendererBackgroundColor(); 587 | 588 | if (storeEvents.getDefaultFullScreen()) { 589 | logger.info('set full screen'); 590 | mainWindow.maximize(); 591 | } 592 | mainWindow.show(); 593 | } 594 | 595 | // Quit when all windows are closed. 596 | app.on('window-all-closed', async () => { 597 | // On macOS, it is common for applications and their menu bar 598 | // to stay active until the user quits explicitly with Cmd + Q 599 | if (process.platform !== 'darwin') { 600 | await dbManage.disconnect(); 601 | app.quit(); 602 | } 603 | }); 604 | 605 | app.on('activate', async () => { 606 | // On macOS, it's common to re-create a window in the app when the 607 | // dock icon is clicked and there are no other windows open. 608 | if (!BrowserWindow.getAllWindows().length) { 609 | await createWindow(); 610 | } 611 | }); 612 | 613 | // This method will be called when Electron has finished 614 | // initialization and is ready to create browser windows. 615 | // Some APIs can only be used after this event occurs. 616 | app.on('ready', async () => { 617 | if (isDevelopment && !process.env.IS_TEST) { 618 | // Install Vue Devtools 619 | try { 620 | const vueDevToolsPath = join( 621 | homedir(), 622 | '/Library/Application Support/Microsoft Edge/Default/Extensions/nhdogjmejiglipccpnnnanhbledajbpd', 623 | ); 624 | await session.defaultSession.loadExtension( 625 | `${vueDevToolsPath}/${readdirSync(vueDevToolsPath)[0]}`, 626 | ); 627 | } catch (e) { 628 | logger.error('Vue Devtools failed to install:', e.toString()); 629 | } 630 | } 631 | await dbManage.resetDb(); 632 | await createWindow(); 633 | }); 634 | 635 | app.on('web-contents-created', (_, webContents) => { 636 | webContents.setWindowOpenHandler(detail => { 637 | shell.openExternal(detail.url).then(); 638 | return { action: 'deny' }; 639 | }); 640 | }); 641 | 642 | // Exit cleanly on request from parent process in development mode. 643 | if (isDevelopment) { 644 | if (process.platform === 'win32') { 645 | process.on('message', async data => { 646 | if (data === 'graceful-exit') { 647 | await dbManage.disconnect(); 648 | app.quit(); 649 | } 650 | }); 651 | } else { 652 | process.on('SIGTERM', async () => { 653 | await dbManage.disconnect(); 654 | app.quit(); 655 | }); 656 | } 657 | } 658 | -------------------------------------------------------------------------------- /src/main-config.js: -------------------------------------------------------------------------------- 1 | const titles = { 2 | agnes: { 3 | list: '药品总库', // 文章列表 4 | menu: '类库', // 长篇合集目录 5 | favorite: '反应记录', // 收藏夹 6 | history: '实验记录', // 历史记录 7 | manage: 'AT总控室', // 元数据管理 8 | copyright: '豚鼠光荣榜', // 鸣谢 9 | name: 'Agnes Tachyon研究所', // 大书库 10 | }, 11 | bourbon: { 12 | list: '作战模拟', // 文章列表 13 | menu: '势力情报', // 长篇合集目录 14 | favorite: '驾驶员休息室', // 收藏夹 15 | history: '作战记录', // 历史记录 16 | manage: '维修室', // 元数据管理 17 | copyright: '状态一览', // 鸣谢 18 | name: '美浦波旁格纳库', // 大书库 19 | }, 20 | cemetery: { 21 | list: '墓群', // 文章列表 22 | menu: '墓主家系', // 长篇合集目录 23 | favorite: '墓志抄录', // 收藏夹 24 | history: '访客登记', // 历史记录 25 | manage: '墓庐', // 元数据管理 26 | copyright: '建园纪念碑', // 鸣谢 27 | name: '森特雷旧事陵园', // 大书库 28 | }, 29 | group: { 30 | list: 'Takatoshi目白城历史博览', // 文章列表 31 | menu: '不知火经典编修', // 长篇合集目录 32 | favorite: '童姥万碎浓厚闷绝', // 收藏夹 33 | history: '鬼道花肥TS调教', // 历史记录 34 | manage: '风之低吟家电维修', // 元数据管理 35 | copyright: 'Nils口腔治疗', // 鸣谢 36 | name: 'NGA赛马娘翻译交流', // 大书库 37 | }, 38 | mejiro: { 39 | list: '书籍森林', // 文章列表 40 | menu: '哺乳室', // 长篇合集目录 41 | favorite: '特别书库', // 收藏夹 42 | history: '个人阅览室', // 历史记录 43 | manage: '管理员室', // 元数据管理 44 | copyright: '建馆纪念碑', // 鸣谢 45 | name: '目白城城立一心同体图书馆', // 大书库 46 | }, 47 | nils: { 48 | list: '大浴池', // 文章列表 49 | menu: '一条龙', // 长篇合集目录 50 | favorite: '半裸休息室', // 收藏夹 51 | history: '浴桶', // 历史记录 52 | manage: '锅炉房', // 元数据管理 53 | copyright: '妙妙屋铭', // 鸣谢 54 | name: 'ChibaNils妙妙屋', // 大书库 55 | }, 56 | oguri: { 57 | list: '单品', // 文章列表 58 | menu: '套餐', // 长篇合集目录 59 | favorite: '外卖', // 收藏夹 60 | history: '账单', // 历史记录 61 | manage: '后厨', // 元数据管理 62 | copyright: '柜台', // 鸣谢 63 | name: '小栗帽的幸福食堂', // 大书库 64 | }, 65 | origin: { 66 | list: '书海', // 文章列表 67 | menu: '总目', // 长篇合集目录 68 | favorite: '个人藏书', // 收藏夹 69 | history: '借阅记录', // 历史记录 70 | manage: '管理处', // 元数据管理 71 | copyright: '建馆纪念碑', // 鸣谢 72 | name: '赛马娘同人集中楼大书库', // 大书库 73 | }, 74 | satono: { 75 | list: '训练员忏悔室', // 文章列表 76 | menu: '圣“器”储藏间', // 长篇合集目录 77 | favorite: '豪华大床', // 收藏夹 78 | history: '地下室', // 历史记录 79 | manage: '光钻倾听室', // 元数据管理 80 | copyright: '教堂纪念碑', // 鸣谢 81 | name: '里见教堂', // 大书库 82 | }, 83 | traincen: { 84 | list: '女帝的插花之道', // 文章列表 85 | menu: '露娜的爆笑笑话集', // 长篇合集目录 86 | favorite: '白仁的纯肉料理教室', // 收藏夹 87 | history: '青竹的风纪处罚记录', // 历史记录 88 | manage: '抄写处', // 元数据管理 89 | copyright: '宣传板', // 鸣谢 90 | name: '特雷森学生会档案室', // 大书库 91 | }, 92 | }; 93 | 94 | module.exports = { 95 | themes: [ 96 | { 97 | color: 'nga', 98 | label: '那我呢', 99 | }, 100 | { 101 | color: 'elui', 102 | label: '特别白', 103 | }, 104 | { 105 | color: 'cyan', 106 | label: '铃鹿青', 107 | }, 108 | { 109 | color: 'teio', 110 | label: '帝王蓝', 111 | }, 112 | { 113 | color: 'purple', 114 | label: '麦昆紫', 115 | }, 116 | { 117 | color: 'black', 118 | label: '玄驹黑', 119 | }, 120 | { 121 | color: 'green', 122 | label: '光钻绿', 123 | }, 124 | { 125 | color: 'exhentai', 126 | label: '荒漠灰', 127 | }, 128 | { 129 | label: '飞鹰粉', 130 | color: 'pink', 131 | }, 132 | { 133 | color: 'porn', 134 | label: '气槽黄', 135 | }, 136 | ], 137 | titles, 138 | }; 139 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import VueQuillEditor from 'vue-quill-editor'; 4 | import ElementUI from 'element-ui'; 5 | 6 | import 'element-ui/lib/theme-chalk/index.css'; 7 | import '@/assets/fonts/font.css'; 8 | 9 | import App from '@/renderer/app.vue'; 10 | import ArticleView from '@/renderer/views/article.vue'; 11 | import ManageView from '@/renderer/views/manage.vue'; 12 | import MenuView from '@/renderer/views/menu.vue'; 13 | import FavoriteView from '@/renderer/views/favorite.vue'; 14 | import HistoryView from '@/renderer/views/history.vue'; 15 | import CopyrightView from '@/renderer/views/copyright.vue'; 16 | import EmptyView from '@/renderer/views/empty.vue'; 17 | 18 | Vue.config.productionTip = false; 19 | Vue.use(VueRouter); 20 | Vue.use(VueQuillEditor); 21 | Vue.use(ElementUI); 22 | 23 | const router = new VueRouter({ 24 | routes: [ 25 | { path: '/', redirect: '/list' }, 26 | { 27 | path: '/list', 28 | name: 'Article', 29 | component: ArticleView, 30 | }, 31 | { 32 | path: '/manage', 33 | name: 'Manage', 34 | component: ManageView, 35 | }, 36 | { path: '/menu/:id', name: 'Menu', component: MenuView }, 37 | { 38 | path: '/favorites', 39 | name: 'Favorite', 40 | component: FavoriteView, 41 | }, 42 | { path: '/history', name: 'History', component: HistoryView }, 43 | { path: '/copyright', name: 'Copyright', component: CopyrightView }, 44 | { 45 | path: '/empty', 46 | name: 'Empty', 47 | component: EmptyView, 48 | }, 49 | ], 50 | }); 51 | 52 | new Vue({ 53 | router, 54 | render: h => h(App), 55 | }).$mount('#app'); 56 | -------------------------------------------------------------------------------- /src/renderer/app.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 430 | 431 | 807 | -------------------------------------------------------------------------------- /src/renderer/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umalib/UmaLibDesktop/f6e58b5e6106b8490f3fd01b1ffc9e16680866b2/src/renderer/images/cover.png -------------------------------------------------------------------------------- /src/renderer/utils/connector.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron'); 2 | const MD5 = new (require('jshashes').MD5)(); 3 | 4 | const id2Resolve = {}; 5 | ipcRenderer.on('artChannel', (_, res) => { 6 | if (id2Resolve[res.id]) { 7 | id2Resolve[res.id](res.data); 8 | } 9 | }); 10 | 11 | module.exports = { 12 | async get(action, args) { 13 | const id = MD5.hex(action + JSON.stringify(args) + new Date().getTime()); 14 | ipcRenderer.send('artChannel', { 15 | id, 16 | action, 17 | args, 18 | }); 19 | return new Promise(resolve => { 20 | id2Resolve[id] = resolve; 21 | }); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/renderer/utils/data.js: -------------------------------------------------------------------------------- 1 | const editorList = [ 2 | '黑羽仙洛', 3 | '风之低吟', 4 | 'ChibaNils', 5 | 'byslm', 6 | 'Alanポポ', 7 | '提里奥·屠鸡者', 8 | '盈盈星尘', 9 | '女人的名字', 10 | 'NagatoSnake', 11 | ]; 12 | 13 | const staffList = [ 14 | 'byslm', 15 | 'ChibaNils', 16 | 'ken啊我', 17 | 'NagatoSnake', 18 | 'nameabcd', 19 | 'STROHEIM', 20 | 'Takatoshi', 21 | 'Tye_sine', 22 | 'z2566687', 23 | '万碎碎碎碎碎', 24 | '千綿奏', 25 | '女人的名字', 26 | '风之低吟', 27 | '东方季枫', 28 | '华生宇', 29 | '进击的南村群童', 30 | '南极洲老土著', 31 | '陨落赤炎', 32 | '莫名的不知火', 33 | '提里奥·屠鸡者', 34 | '黑羽仙洛', 35 | ].filter((v, i, l) => { 36 | return l.indexOf(v) === i; 37 | }); 38 | 39 | module.exports = { 40 | editors: editorList, 41 | elTagTypes: ['info', '', 'warning', 'success', 'danger'], 42 | signInfo: { 43 | content: '内容:NGA赛马娘翻译交流群 | 开发:风之低吟 | 版本:3.0.2-Final', 44 | pubKey: 45 | '04f7c5d1bf43e06c4a119deb999c33a488fc38d1a7f6387cdc0001ed190d6b304846b3d2931fb15f819c6e57ac7ce119f8c68e376a5631d5ccfc1f712a51187123', 46 | sign: 47 | '3044022046198076647c9469c0cbd0cffe134dc208d6d0337f6c7cace46a3d0981dc0839022068d1b2aa8b71d1b8b642e30b1b3a7f2bd160c096d4307216f53943fb5e9438f7', 48 | }, 49 | staffs: staffList, 50 | tagTypes: ['其他', '角色', '系列', '长篇/合集', '争议/不适'], 51 | }; 52 | -------------------------------------------------------------------------------- /src/renderer/utils/renderer-utils.js: -------------------------------------------------------------------------------- 1 | const formatter = new Intl.DateTimeFormat('cn', { 2 | dateStyle: 'short', 3 | timeStyle: 'short', 4 | hour12: false, 5 | }); 6 | 7 | module.exports = { 8 | addNewSourceInTextObj(obj) { 9 | obj.source.push({ val: '' }); 10 | }, 11 | formatTimeStamp(timestamp) { 12 | if (!timestamp) { 13 | return ''; 14 | } 15 | return formatter.format(new Date(timestamp)).replace(/\//g, '-'); 16 | }, 17 | getNewTextObj() { 18 | return { 19 | author: '', 20 | content: '', 21 | name: '', 22 | note: '', 23 | source: [{ val: '' }], 24 | tags: [], 25 | translator: '', 26 | uploadTime: new Date().getTime(), 27 | }; 28 | }, 29 | initSelectedArtObj() { 30 | return { 31 | author: '', 32 | id: -1, 33 | name: '', 34 | note: '', 35 | source: [], 36 | tagLabels: [], 37 | tags: [], 38 | translator: '', 39 | }; 40 | }, 41 | removeSourceInTextObj(obj, index) { 42 | if (obj.source.length === 1) { 43 | obj.source = [{ val: '' }]; 44 | } else { 45 | obj.source.splice(index, 1); 46 | } 47 | }, 48 | splitList(src, size) { 49 | const ret = []; 50 | let tmpList = [], 51 | i = 0; 52 | do { 53 | tmpList.push(src[i]); 54 | i++; 55 | if (i % size === 0 || i === src.length) { 56 | ret.push(tmpList); 57 | tmpList = []; 58 | } 59 | } while (i < src.length); 60 | return ret; 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /src/renderer/views/copyright.vue: -------------------------------------------------------------------------------- 1 | 158 | 159 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/renderer/views/empty.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/renderer/views/favorite.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /src/renderer/views/history.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/renderer/views/manage.vue: -------------------------------------------------------------------------------- 1 | 136 | 137 | 405 | 406 | 416 | -------------------------------------------------------------------------------- /src/renderer/views/menu.vue: -------------------------------------------------------------------------------- 1 | 120 | 121 | 259 | 260 | 294 | -------------------------------------------------------------------------------- /src/renderer/views/sub-components/article-table.vue: -------------------------------------------------------------------------------- 1 | 334 | 335 | 376 | 377 | 378 | -------------------------------------------------------------------------------- /src/renderer/views/sub-components/pub-article.vue: -------------------------------------------------------------------------------- 1 | 170 | 171 | 231 | 232 | 263 | -------------------------------------------------------------------------------- /src/renderer/views/sub-components/random-articles.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/renderer/views/sub-components/recommended-articles.vue: -------------------------------------------------------------------------------- 1 | 307 | 308 | 351 | 352 | 383 | -------------------------------------------------------------------------------- /src/renderer/views/sub-components/show-article.vue: -------------------------------------------------------------------------------- 1 | 160 | 161 | 250 | 251 | 320 | -------------------------------------------------------------------------------- /tools/androidTransfer.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { copyFileSync } = require('fs'); 3 | const { resolve } = require('path'); 4 | const { path } = require('./config.js'); 5 | const logger = require('log4js').getLogger('transfer'); 6 | logger.level = 'info'; 7 | 8 | const android = resolve( 9 | path.replace(/data[/|\\](slib[/|\\])?/, 'data/android/'), 10 | ); 11 | logger.info(`transfer ${path} to ${android}`); 12 | copyFileSync(path, android); 13 | logger.info('copy done'); 14 | 15 | const prisma = new PrismaClient({ 16 | datasources: { db: { url: `file:${resolve(android)}` } }, 17 | }); 18 | 19 | function countBase64(content) { 20 | const result = content.match(/]*">/g); 21 | return result ? result.length : 0; 22 | } 23 | 24 | async function task() { 25 | await prisma.$queryRaw`vacuum`; 26 | let tagList = ( 27 | await prisma.tag.findMany({ 28 | where: { 29 | OR: [ 30 | { 31 | name: { 32 | in: ['R18', 'AA', '安科', '十八禁'], 33 | }, 34 | }, 35 | { 36 | type: 4, 37 | }, 38 | ], 39 | }, 40 | }) 41 | ).map(tag => tag.id); 42 | let artList = ( 43 | await prisma['tagged'].findMany({ where: { tagId: { in: tagList } } }) 44 | ) 45 | .map(tagged => tagged.artId) 46 | .filter((x, i, l) => i === l.indexOf(x)); 47 | await prisma['tagged'].deleteMany({ 48 | where: { OR: [{ artId: { in: artList } }, { tagId: { in: tagList } }] }, 49 | }); 50 | 51 | await prisma.article.deleteMany({ where: { id: { in: artList } } }); 52 | logger.info(`remove ${artList.length} articles: ${artList.join(', ')}`); 53 | 54 | tagList = ( 55 | await prisma.tag.findMany({ 56 | where: { 57 | taggedList: { 58 | none: {}, 59 | }, 60 | }, 61 | }) 62 | ).map(tag => tag.id); 63 | await prisma.tag.deleteMany({ where: { id: { in: tagList } } }); 64 | logger.info(`remove ${tagList.length} tags: ${tagList.join(', ')}`); 65 | await prisma.tag.deleteMany({ 66 | where: { 67 | taggedList: { 68 | none: {}, 69 | }, 70 | }, 71 | }); 72 | await prisma.tag.updateMany({ 73 | data: { 74 | cover: '', 75 | description: '', 76 | }, 77 | }); 78 | let count = 0; 79 | artList = await prisma.article.findMany(); 80 | for (const art of artList) { 81 | const tmpCount = countBase64(art.content); 82 | if (tmpCount > 0) { 83 | count += tmpCount; 84 | logger.info(`${art.id} [${art.name}]\t${tmpCount}`); 85 | art.content = art.content.replace( 86 | /]*>/g, 87 | '[图片]', 88 | ); 89 | await prisma.article.update({ 90 | data: { content: art.content }, 91 | where: { id: art.id }, 92 | }); 93 | } 94 | } 95 | 96 | artList = (await prisma.article.findMany({ select: { id: true } })).map( 97 | x => x.id, 98 | ); 99 | tagList = (await prisma.tag.findMany({ select: { id: true } })).map( 100 | x => x.id, 101 | ); 102 | const dict = await prisma.dict.findMany(); 103 | const toRemoveDictList = []; 104 | for (const entry of dict) { 105 | if (entry.refId && tagList.indexOf(entry.refId) === -1) { 106 | toRemoveDictList.push(entry.id); 107 | } else if (entry.relatedId && artList.indexOf(entry.relatedId) === -1) { 108 | await prisma.dict.update({ 109 | where: { id: entry.id }, 110 | data: { 111 | related: '', 112 | relatedId: 0, 113 | }, 114 | }); 115 | } 116 | } 117 | await prisma.dict.deleteMany({ 118 | where: { 119 | id: { 120 | in: toRemoveDictList, 121 | }, 122 | }, 123 | }); 124 | 125 | const recs = await prisma['rec'].findMany(); 126 | const toRemovedRecList = recs 127 | .filter( 128 | rec => 129 | rec.r || 130 | (rec.type >= 2 && rec.type <= 4 && tagList.indexOf(rec.refId) === -1) || 131 | (rec.type === 5 && artList.indexOf(rec.refId) === -1), 132 | ) 133 | .map(rec => rec.id); 134 | await prisma['rec'].deleteMany({ 135 | where: { 136 | id: { 137 | in: toRemovedRecList, 138 | }, 139 | }, 140 | }); 141 | logger.info(`art count: ${artList.length}; base64 count: ${count}`); 142 | logger.info('clean done'); 143 | await prisma.$queryRaw`vacuum;`; 144 | logger.info('vacuum done'); 145 | await prisma.$disconnect(); 146 | } 147 | 148 | task().then(() => logger.info('task done!')); 149 | -------------------------------------------------------------------------------- /tools/base64exporter.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { writeFileSync } = require('fs'); 3 | const { join, resolve } = require('path'); 4 | const { coverPath, imgPath, path } = require('./config.js'); 5 | const MD5 = new (require('jshashes').MD5)(); 6 | const logger = require('log4js').getLogger('exporter'); 7 | logger.level = 'info'; 8 | 9 | logger.info(`export base64 images in ${path}`); 10 | logger.info(`article image path=${imgPath}`); 11 | logger.info(`tag cover path=${coverPath}`); 12 | 13 | const prisma = new PrismaClient({ 14 | datasources: { db: { url: `file:${join(resolve(path))}` } }, 15 | }); 16 | 17 | function countBase64(content) { 18 | const result = content.match( 19 | /]*>/g, 20 | ); 21 | return result ? result : []; 22 | } 23 | 24 | const md5dict = {}; 25 | 26 | function saveFile(src, name, path) { 27 | const suffix = src.match(/image\/\w+/)[0].substring(6); 28 | const base64 = src.match(/base64,[^"]+/)[0].substring(7); 29 | const hex = MD5.hex(base64); 30 | if (md5dict[hex]) { 31 | return { hex, file: md5dict[hex] }; 32 | } else { 33 | const dataBuffer = Buffer.from(base64, 'base64'); 34 | const fileName = `${path}${name}.${suffix}`; 35 | md5dict[hex] = fileName; 36 | writeFileSync(fileName, dataBuffer); 37 | return { hex, file: fileName }; 38 | } 39 | } 40 | 41 | async function task() { 42 | let count = 0; 43 | const artList = await prisma.article.findMany(); 44 | let csv = ''; 45 | for (const art of artList) { 46 | const imageArr = countBase64(art.content); 47 | if (imageArr.length > 0) { 48 | count += imageArr.length; 49 | logger.info(`${art.id} [${art.name}]\t${imageArr.length}`); 50 | for (let i = 0; i < imageArr.length; ++i) { 51 | const imgStr = imageArr[i]; 52 | const outFile = saveFile(imgStr, `${art.id}-${i + 1}`, imgPath); 53 | csv += `"Image ${art.id}-${i + 1}","${outFile.hex}","${ 54 | outFile.file 55 | }","${art.name}","${new Date( 56 | art.uploadTime * 1000, 57 | ).toLocaleString()}","${art.translator || art.author}"\r\n`; 58 | logger.info(`image ${art.id}-${i + 1}: ${outFile.file}`); 59 | } 60 | } 61 | } 62 | (await prisma.tag.findMany()).forEach(tag => { 63 | if (tag.cover && tag.cover.startsWith('data:image')) { 64 | count++; 65 | const outFile = saveFile(tag.cover, tag.name, coverPath); 66 | csv += `"${tag.name}",,"${outFile.file}"\n`; 67 | logger.info(`image ${tag.name}: ${outFile.file}`); 68 | } 69 | }); 70 | writeFileSync(`${imgPath}/dict.csv`, `\uFEFF${csv}`, { 71 | encoding: 'utf8', 72 | }); 73 | logger.info(`all: ${count}`); 74 | await prisma.$disconnect(); 75 | } 76 | 77 | task().then(() => logger.info('task done!')); 78 | -------------------------------------------------------------------------------- /tools/base64modifier.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { readFileSync } = require('fs'); 3 | const { join, resolve } = require('path'); 4 | const { imgPath, path } = require('./config.js'); 5 | const MD5 = new (require('jshashes').MD5)(); 6 | const logger = require('log4js').getLogger('modifier'); 7 | logger.level = 'info'; 8 | 9 | logger.info(`modify images in ${path}, dictionary=${imgPath}/dict.csv`); 10 | 11 | const prisma = new PrismaClient({ 12 | datasources: { db: { url: `file:${join(resolve(path))}` } }, 13 | }); 14 | 15 | const md5dict = {}; 16 | 17 | function removeBase64(content) { 18 | const result = 19 | content.match(/]*>/g) || []; 20 | let ret = content; 21 | let count = 0; 22 | result.forEach(reg => { 23 | const base64 = reg.match(/base64,[^"]+/)[0].substring(7); 24 | const hex = MD5.hex(base64); 25 | if (md5dict[hex]) { 26 | count++; 27 | ret = ret.replace( 28 | reg.match(/data:image\/\w+;base64,[^"]+/)[0], 29 | md5dict[hex], 30 | ); 31 | } 32 | }); 33 | return { ret, count }; 34 | } 35 | 36 | function removeQuote(src) { 37 | if (src.startsWith('"')) { 38 | return eval(src); 39 | } 40 | return src; 41 | } 42 | 43 | async function task() { 44 | const csv = readFileSync(`${imgPath}/dict.csv`) 45 | .toString() 46 | .split('\r\n'); 47 | const tag2Id = {}; 48 | (await prisma.tag.findMany({ where: { type: 3 } })).forEach( 49 | tag => (tag2Id[tag.name] = tag.id), 50 | ); 51 | for (const line of csv) { 52 | const lineArr = line.split(','); 53 | if (lineArr.length < 3) { 54 | continue; 55 | } 56 | const name = removeQuote(lineArr[0]), 57 | hash = removeQuote(lineArr[1]), 58 | url = removeQuote(lineArr[2]); 59 | if (url.startsWith('http')) { 60 | if (name.indexOf('Image ') !== -1) { 61 | md5dict[hash] = url; 62 | } else { 63 | logger.info(name, url); 64 | await prisma.tag.update({ 65 | data: { cover: url }, 66 | where: { id: tag2Id[name] }, 67 | }); 68 | } 69 | } 70 | } 71 | const artList = await prisma.article.findMany(); 72 | for (const art of artList) { 73 | const content = removeBase64(art.content); 74 | if (art.content.length !== content.ret.length) { 75 | await prisma.article.update({ 76 | data: { content: content.ret }, 77 | where: { id: art.id }, 78 | }); 79 | logger.info( 80 | `[${art.id}] ${art.name}: remove ${content.count} base64 files`, 81 | ); 82 | } 83 | } 84 | logger.info('clean done'); 85 | await prisma.$queryRaw`vacuum`; 86 | logger.info('vacuum done'); 87 | await prisma.$disconnect(); 88 | } 89 | 90 | task().then(() => logger.info('task done!')); 91 | -------------------------------------------------------------------------------- /tools/contentCleaner.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { join, resolve } = require('path'); 3 | const { alignCenterImg, path } = require('./config.js'); 4 | const logger = require('log4js').getLogger('cleaner'); 5 | logger.level = 'info'; 6 | 7 | logger.info(`clean ${path}, set image align to center? ${alignCenterImg}`); 8 | 9 | const prisma = new PrismaClient({ 10 | datasources: { 11 | db: { 12 | url: `file:${join(resolve(path))}`, 13 | }, 14 | }, 15 | }); 16 | 17 | function cleanDuplicateLineBreak(content) { 18 | for ( 19 | let tmp = content.replace(/

\s*\s*<\/p>/g, '\n'); 20 | tmp.indexOf('

') === -1 && tmp.substring(3).indexOf('

') !== -1; 21 | tmp = content.replace(/

\s*\s*<\/p>/g, '\n') 22 | ) { 23 | content = content.replace(/<\/p>\s*

\s*\s*<\/p>/g, '

'); 24 | } 25 | return content; 26 | } 27 | 28 | function cleanBlank(src) { 29 | return src 30 | .replace(/^\s*/, '') 31 | .replace(/\s*$/, '') 32 | .replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, ''); 33 | } 34 | 35 | async function task() { 36 | const articles = await prisma.article.findMany(); 37 | for (const art of articles) { 38 | let content = art.content; 39 | if ( 40 | art.note.indexOf('字符画') === -1 && 41 | art.note.indexOf('AA漫画') === -1 42 | ) { 43 | content = content.replace(/ /g, ' '); 44 | content = content.replace( 45 | /\s*color:\s*(black|rgb\(51,\s*51,\s*51\)|rgb\(16,\s*39,\s*63\));\s*/g, 46 | '', 47 | ); 48 | content = content.replace(/\s+style="\s*"\s*/g, ''); 49 | content = content 50 | .replace(/^\s*(

\s*\s*<\/p>\s*)*\s*/, '') 51 | .replace(/\s*(\s*

\s*\s*<\/p>)*\s*$/, '') 52 | .replace(/\s*

\s*\s*<\/p>\s*/g, '


'); 53 | content = content.replace(/<\/p>\s*

/g, '

'); 54 | content = cleanDuplicateLineBreak(content); 55 | content = content.replace(/\.(thumb|medium).jpg"/g, '"'); 56 | content = content.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, ''); 57 | if (alignCenterImg) { 58 | let alignCenterImg = '

<'; 59 | alignCenterImg += 'img '; 60 | content = content.replace(/

\s* ${name}]`); 112 | await prisma.tag.update({ 113 | data: { 114 | name, 115 | }, 116 | where: { 117 | id: tag.id, 118 | }, 119 | }); 120 | } 121 | } 122 | logger.info('cleaning tags done!'); 123 | await prisma['tagged'].deleteMany({ 124 | where: { 125 | OR: [ 126 | { 127 | NOT: { 128 | artId: { 129 | in: articles.map(x => x.id), 130 | }, 131 | }, 132 | }, 133 | { 134 | NOT: { 135 | tagId: { 136 | in: tags.map(x => x.id), 137 | }, 138 | }, 139 | }, 140 | ], 141 | }, 142 | }); 143 | logger.info('cleaning tagged done!'); 144 | logger.info('clean done!'); 145 | await prisma.$queryRaw`vacuum;`; 146 | logger.info('vacuum done!'); 147 | await prisma.$disconnect(); 148 | } 149 | 150 | task().then(() => logger.info('task done!')); 151 | -------------------------------------------------------------------------------- /tools/copyrightGenerator.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { resolve } = require('path'); 3 | const { path } = require('./config.js'); 4 | const { staffs, editors } = require('../src/renderer/utils/data'); 5 | const logger = require('log4js').getLogger('generator'); 6 | logger.level = 'info'; 7 | 8 | logger.info(`check creators in ${path}`); 9 | 10 | const prisma = new PrismaClient({ 11 | datasources: { 12 | db: { 13 | url: `file:${resolve(path)}`, 14 | }, 15 | }, 16 | }); 17 | 18 | async function task() { 19 | const creators = {}; 20 | const articles = await prisma.article.findMany({ 21 | select: { 22 | id: true, 23 | author: true, 24 | translator: true, 25 | content: true, 26 | }, 27 | }); 28 | await prisma.$disconnect(); 29 | articles.forEach(x => { 30 | const creator = x.translator ? x.translator : x.author; 31 | creator.split('/').forEach(c => (creators[c] = true)); 32 | }); 33 | staffs.forEach(c => delete creators[c]); 34 | editors.forEach(c => delete creators[c]); 35 | delete creators['我就是雷gay的化身']; 36 | delete creators['匿名']; 37 | delete creators['超合金魂DX紅美鈴']; 38 | delete creators['南村群童']; 39 | delete creators['亚岚']; 40 | delete creators['Nils']; 41 | delete creators['ken']; 42 | delete creators['病娇的芙兰']; 43 | delete creators['风拂云默']; 44 | delete creators['RiverOfCrystals']; 45 | creators['起啥不是个名字(病娇的芙兰)'] = true; 46 | creators['文甄心(风拂云默)'] = true; 47 | creators['RiverOfCrystals(BPK5uQqUk)'] = true; 48 | logger.info(Object.keys(creators).length); 49 | for (const k in creators) { 50 | console.log(k); 51 | } 52 | } 53 | 54 | task().then(() => logger.info('task done!')); 55 | -------------------------------------------------------------------------------- /tools/coverSetter.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const fs = require('fs'); 3 | const { join, resolve } = require('path'); 4 | const { path, coverPath } = require('./config.js'); 5 | const logger = require('log4js').getLogger('setter'); 6 | logger.level = 'info'; 7 | 8 | logger.info(`set tag covers of ${path} from ${coverPath}`); 9 | 10 | const prisma = new PrismaClient({ 11 | datasources: { 12 | db: { 13 | url: `file:${join(resolve(path))}`, 14 | }, 15 | }, 16 | }); 17 | 18 | async function task() { 19 | const id2Tag = {}; 20 | (await prisma.tag.findMany()).forEach(tag => (id2Tag[tag.name] = tag.id)); 21 | const fileList = fs.readdirSync(coverPath); 22 | for (const fileName of fileList) { 23 | const fileNameArr = fileName.split('.'); 24 | const name = fileNameArr[0], 25 | suffix = fileNameArr[1]; 26 | if (id2Tag[name]) { 27 | const buffer = Buffer.from( 28 | fs.readFileSync(coverPath + fileName, 'binary'), 29 | 'binary', 30 | ); 31 | const coverData = `data:image/${suffix};base64,${buffer.toString( 32 | 'base64', 33 | )}`; 34 | await prisma.tag.update({ 35 | data: { 36 | cover: coverData, 37 | }, 38 | where: { 39 | id: id2Tag[name], 40 | }, 41 | }); 42 | logger.info(`${id2Tag[name]}/${name}: ${coverData.length}`); 43 | } else { 44 | logger.error(fileName); 45 | } 46 | } 47 | logger.info('set done'); 48 | await prisma.$queryRaw`vacuum`; 49 | logger.info('vacuum done'); 50 | await prisma.$disconnect(); 51 | } 52 | 53 | task().then(() => logger.info('task done!')); 54 | -------------------------------------------------------------------------------- /tools/creatorChecker.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { join, resolve } = require('path'); 3 | const { path } = require('./config.js'); 4 | const { creatorSortBy } = require('./config'); 5 | const logger = require('log4js').getLogger('checker'); 6 | logger.level = 'info'; 7 | 8 | logger.info(`check creators in ${path}`); 9 | 10 | const prisma = new PrismaClient({ 11 | datasources: { 12 | db: { 13 | url: `file:${join(resolve(path))}`, 14 | }, 15 | }, 16 | }); 17 | 18 | async function task() { 19 | const creators = {}; 20 | 21 | const lengthFilter = {}; 22 | ( 23 | await prisma.tag.findMany({ 24 | where: { 25 | name: { 26 | in: ['AA', '安科'], 27 | }, 28 | }, 29 | include: { 30 | taggedList: true, 31 | }, 32 | }) 33 | ).forEach(x => x.taggedList.forEach(y => (lengthFilter[y.artId] = 1))); 34 | 35 | const articles = await prisma.article.findMany({ 36 | select: { 37 | id: true, 38 | author: true, 39 | translator: true, 40 | content: true, 41 | }, 42 | }); 43 | articles.forEach(x => { 44 | const creator = x.translator ? x.translator : x.author; 45 | const creatorArr = creator.split('/'); 46 | const content = x.content 47 | .replace(/<[^>]*>/g, '') 48 | .replace( 49 | /[。?!,、;:“”‘’「」()[\]〔〕【】『』—…·~~《》〈〉_/.?!,;:"'<>()@#$%^&*+=\\`\s]/g, 50 | '', 51 | ).length; 52 | creatorArr.forEach(c => { 53 | if (!creators[c]) { 54 | creators[c] = { 55 | count: 0, 56 | content: 0, 57 | }; 58 | } 59 | creators[c].count += 1; 60 | creators[c].content += lengthFilter[x.id] ? 0 : content; 61 | }); 62 | }); 63 | const creatorArr = Object.keys(creators).map(x => { 64 | return { 65 | name: x, 66 | count: creators[x].count, 67 | content: creators[x].content, 68 | }; 69 | }); 70 | if (creatorSortBy === 'count') { 71 | creatorArr.sort((a, b) => { 72 | if (a.count === b.count) { 73 | if (a.content === b.content) { 74 | return a.name > b.name ? 1 : -1; 75 | } 76 | return b.content - a.content; 77 | } 78 | return b.count - a.count; 79 | }); 80 | } else { 81 | creatorArr.sort((a, b) => { 82 | if (a.content === b.content) { 83 | if (a.count === b.count) { 84 | return a.name > b.name ? 1 : -1; 85 | } 86 | return a.count - b.count; 87 | } 88 | return b.content - a.content; 89 | }); 90 | } 91 | creatorArr.forEach(x => 92 | logger.info(`${x.name}\t${x.count}\t${x.content.toLocaleString()}`), 93 | ); 94 | await prisma.$disconnect(); 95 | } 96 | 97 | task().then(() => logger.info('task done!')); 98 | -------------------------------------------------------------------------------- /tools/dataAnalyser.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { writeFileSync } = require('fs'); 3 | const { resolve } = require('path'); 4 | const { path } = require('./config.js'); 5 | const logger = require('log4js').getLogger('analyser'); 6 | logger.level = 'info'; 7 | 8 | logger.info(`analyze article data in db ${path}`); 9 | 10 | const prisma = new PrismaClient({ 11 | datasources: { 12 | db: { 13 | url: `file:${resolve(path)}`, 14 | }, 15 | }, 16 | }); 17 | 18 | async function task() { 19 | const articles = ( 20 | await prisma.article.findMany({ 21 | select: { 22 | id: true, 23 | content: true, 24 | author: true, 25 | translator: true, 26 | uploadTime: true, 27 | }, 28 | }) 29 | ).map(x => { 30 | return { 31 | id: x.id, 32 | creator: x.translator || x.author, 33 | content: x.content 34 | .replace(/<[^>]*>/g, '') 35 | .replace( 36 | /[。?!,、;:“”‘’「」()[\]〔〕【】『』—…·~~《》〈〉_/.?!,;:"'<>()@#$%^&*+=\\`\s]/g, 37 | '', 38 | ).length, 39 | uploadTime: Math.floor((x.uploadTime + 8 * 3600) / 86400), 40 | }; 41 | }); 42 | 43 | const tags = await prisma.tag.findMany({ 44 | include: { 45 | taggedList: true, 46 | }, 47 | }); 48 | 49 | const lengthFilter = {}; 50 | tags 51 | .filter(x => x.name === 'AA' || x.name === '安科') 52 | .forEach(x => x.taggedList.forEach(y => (lengthFilter[y.artId] = 1))); 53 | 54 | let creatorDict = {}; 55 | articles.forEach(x => { 56 | if (!creatorDict[x.creator]) { 57 | creatorDict[x.creator] = { 58 | count: 0, 59 | length: 0, 60 | }; 61 | } 62 | creatorDict[x.creator].count += 1; 63 | creatorDict[x.creator].length += lengthFilter[x.id] ? 0 : x.content; 64 | }); 65 | creatorDict = Object.keys(creatorDict).map(x => { 66 | return { 67 | name: x, 68 | count: creatorDict[x].count, 69 | length: creatorDict[x].length, 70 | }; 71 | }); 72 | creatorDict.sort((a, b) => { 73 | if (b.count === a.count) { 74 | if (b.length === a.length) { 75 | return a.name > b.name ? 1 : -1; 76 | } 77 | return b.length - a.length; 78 | } 79 | return b.count - a.count; 80 | }); 81 | const creatorDictCount = creatorDict.slice(0, 10).map(x => x.name); 82 | 83 | creatorDict.sort((a, b) => { 84 | if (b.length === a.length) { 85 | if (b.count === a.count) { 86 | return a.name > b.name ? 1 : -1; 87 | } 88 | return a.count - b.count; 89 | } 90 | return b.length - a.length; 91 | }); 92 | const creatorDictLen = creatorDict.slice(0, 10).map(x => x.name); 93 | 94 | const novelDict = {}; 95 | tags 96 | .filter(x => x.type === 3) 97 | .forEach(x => x.taggedList.forEach(y => (novelDict[y.artId] = true))); 98 | 99 | let tagDict = tags 100 | .filter(x => x.type === 1) 101 | .map(x => { 102 | return { 103 | n: x.name, 104 | c: x.taggedList.filter(y => !novelDict[y.artId]).length, 105 | }; 106 | }); 107 | tagDict.sort((a, b) => { 108 | if (b.c === a.c) { 109 | return a.n > b.n ? 1 : -1; 110 | } 111 | return b.c - a.c; 112 | }); 113 | tagDict = tagDict.slice(0, 10).map(x => x.n); 114 | tagDict = [...tagDict, 'R18', 'R15']; 115 | 116 | const artDict = {}; 117 | tags 118 | .filter(x => tagDict.indexOf(x.name) !== -1) 119 | .forEach(x => { 120 | const tagIndex = tagDict.indexOf(x.name); 121 | x.taggedList.forEach(y => { 122 | if (!novelDict[y.artId] || tagIndex > 9) { 123 | if (!artDict[y.artId]) { 124 | artDict[y.artId] = []; 125 | for (let i = 0; i < tagDict.length; ++i) { 126 | artDict[y.artId].push(0); 127 | } 128 | } 129 | artDict[y.artId][tagIndex] = 1; 130 | } 131 | }); 132 | }); 133 | 134 | const day2count = {}; 135 | for (const art of articles) { 136 | const key = art.uploadTime; 137 | if (!day2count[key]) { 138 | day2count[key] = { 139 | count: { 140 | all: 0, 141 | delta: 0, 142 | creators: [], 143 | tags: [], 144 | }, 145 | len: { 146 | all: 0, 147 | delta: 0, 148 | creators: [], 149 | tags: [], 150 | }, 151 | }; 152 | for (let i = 0; i < creatorDictCount.length; ++i) { 153 | day2count[key].count.creators.push(0); 154 | day2count[key].len.creators.push(0); 155 | } 156 | for (let i = 0; i < tagDict.length; ++i) { 157 | day2count[key].count.tags.push(0); 158 | day2count[key].len.tags.push(0); 159 | } 160 | } 161 | day2count[key].count.delta += 1; 162 | day2count[key].len.delta += lengthFilter[art.id] ? 0 : art.content; 163 | art.creator.split('/').forEach(x => { 164 | const creatorCountIndex = creatorDictCount.indexOf(x); 165 | if (creatorCountIndex >= 0) { 166 | day2count[key].count.creators[creatorCountIndex] += 1; 167 | } 168 | if (!lengthFilter[art.id]) { 169 | const creatorLenIndex = creatorDictLen.indexOf(x); 170 | if (creatorLenIndex >= 0) { 171 | day2count[key].len.creators[creatorLenIndex] += art.content; 172 | } 173 | } 174 | }); 175 | if (artDict[art.id]) { 176 | for (let i = 0; i < tagDict.length; ++i) { 177 | day2count[key].count.tags[i] += artDict[art.id][i]; 178 | day2count[key].len.tags[i] += 179 | artDict[art.id][i] && !lengthFilter[art.id] ? art.content : 0; 180 | } 181 | } 182 | } 183 | 184 | const days = Object.keys(day2count).map(x => Number(x)); 185 | days.sort(); 186 | for (let i = 0; i < days.length; ++i) { 187 | if (i) { 188 | day2count[days[i]].count.all = 189 | day2count[days[i]].count.delta + day2count[days[i - 1]].count.all; 190 | for (let j = 0; j < creatorDictCount.length; ++j) { 191 | day2count[days[i]].count.creators[j] += 192 | day2count[days[i - 1]].count.creators[j]; 193 | } 194 | for (let j = 0; j < tagDict.length; ++j) { 195 | day2count[days[i]].count.tags[j] += 196 | day2count[days[i - 1]].count.tags[j]; 197 | } 198 | day2count[days[i]].len.all = 199 | day2count[days[i]].len.delta + day2count[days[i - 1]].len.all; 200 | for (let j = 0; j < creatorDictCount.length; ++j) { 201 | day2count[days[i]].len.creators[j] += 202 | day2count[days[i - 1]].len.creators[j]; 203 | } 204 | for (let j = 0; j < tagDict.length; ++j) { 205 | day2count[days[i]].len.tags[j] += day2count[days[i - 1]].len.tags[j]; 206 | } 207 | } else { 208 | day2count[days[i]].count.all = day2count[days[i]].count.delta; 209 | day2count[days[i]].len.all = day2count[days[i]].len.delta; 210 | } 211 | } 212 | 213 | const out = { 214 | count: { 215 | all: [], 216 | delta: [], 217 | creators: [], 218 | tags: [], 219 | }, 220 | len: { 221 | all: [], 222 | delta: [], 223 | creators: [], 224 | tags: [], 225 | }, 226 | }; 227 | for (let i = 0; i < creatorDictCount.length; ++i) { 228 | out.count.creators.push([]); 229 | out.len.creators.push([]); 230 | } 231 | for (let i = 0; i < tagDict.length; ++i) { 232 | out.count.tags.push([]); 233 | out.len.tags.push([]); 234 | } 235 | for (const key of days) { 236 | out.count.all.push(day2count[key].count.all); 237 | out.count.delta.push(day2count[key].count.delta); 238 | for (let i = 0; i < creatorDictCount.length; ++i) { 239 | out.count.creators[i].push(day2count[key].count.creators[i]); 240 | } 241 | for (let i = 0; i < tagDict.length; ++i) { 242 | out.count.tags[i].push(day2count[key].count.tags[i]); 243 | } 244 | out.len.all.push(day2count[key].len.all); 245 | out.len.delta.push(day2count[key].len.delta); 246 | for (let i = 0; i < creatorDictCount.length; ++i) { 247 | out.len.creators[i].push(day2count[key].len.creators[i]); 248 | } 249 | for (let i = 0; i < tagDict.length; ++i) { 250 | out.len.tags[i].push(day2count[key].len.tags[i]); 251 | } 252 | } 253 | writeFileSync( 254 | resolve('./result/output-plot-count.json'), 255 | JSON.stringify({ 256 | days, 257 | all: out.count.all, 258 | delta: out.count.delta, 259 | creators: out.count.creators, 260 | tags: out.count.tags, 261 | dict: { 262 | creators: creatorDictCount, 263 | tags: tagDict, 264 | }, 265 | }), 266 | ); 267 | writeFileSync( 268 | resolve('./result/output-plot-len.json'), 269 | JSON.stringify({ 270 | days, 271 | all: out.len.all, 272 | delta: out.len.delta, 273 | creators: out.len.creators, 274 | tags: out.len.tags, 275 | dict: { 276 | creators: creatorDictLen, 277 | tags: tagDict, 278 | }, 279 | }), 280 | ); 281 | await prisma.$disconnect(); 282 | } 283 | 284 | task().then(() => logger.info('task done!')); 285 | -------------------------------------------------------------------------------- /tools/dataCleaner.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { join, resolve } = require('path'); 3 | const { path } = require('./config.js'); 4 | const logger = require('log4js').getLogger('cleaner'); 5 | logger.level = 'info'; 6 | 7 | logger.info(`clean unused data in ${path}`); 8 | 9 | const prisma = new PrismaClient({ 10 | datasources: { 11 | db: { 12 | url: `file:${join(resolve(path))}`, 13 | }, 14 | }, 15 | }); 16 | 17 | async function task() { 18 | const articles = ( 19 | await prisma.article.findMany({ 20 | select: { 21 | id: true, 22 | }, 23 | }) 24 | ).map(x => x.id); 25 | await prisma['tagged'].deleteMany({ 26 | where: { 27 | NOT: { 28 | artId: { 29 | in: articles, 30 | }, 31 | }, 32 | }, 33 | }); 34 | const taggedList = (await prisma['tagged'].findMany()) 35 | .map(x => x.tagId) 36 | .filter((x, i, l) => i === l.indexOf(x)); 37 | await prisma.tag.deleteMany({ 38 | where: { 39 | NOT: { 40 | id: { 41 | in: taggedList, 42 | }, 43 | }, 44 | }, 45 | }); 46 | logger.info('clean done!'); 47 | await prisma.$queryRaw`vacuum;`; 48 | logger.info('vacuum done!'); 49 | await prisma.$disconnect(); 50 | } 51 | 52 | task().then(() => logger.info('task done!')); 53 | -------------------------------------------------------------------------------- /tools/dbMerger.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { join, resolve } = require('path'); 3 | const dbManage = require('../src/db-manage'); 4 | const { path, srcPath } = require('./config.js'); 5 | const logger = require('log4js').getLogger('merger'); 6 | logger.level = 'info'; 7 | 8 | logger.info(`merge ${srcPath} into ${path}`); 9 | 10 | async function task() { 11 | const prisma = new PrismaClient({ 12 | datasources: { 13 | db: { 14 | url: `file:${join(resolve(srcPath))}`, 15 | }, 16 | }, 17 | }); 18 | const id2Tag = {}; 19 | (await prisma.tag.findMany()).forEach(tag => (id2Tag[tag.id] = tag.name)); 20 | const artList = await prisma.article.findMany({ 21 | include: { 22 | taggedList: true, 23 | }, 24 | orderBy: { 25 | uploadTime: 'asc', 26 | }, 27 | }); 28 | await prisma.$disconnect(); 29 | logger.info(`get ${artList.length} articles from source db`); 30 | await dbManage.changeDb(path); 31 | let i = 0; 32 | for (const art of artList) { 33 | let source = art.source; 34 | if (typeof source === 'string') { 35 | source = [{ val: source }]; 36 | } 37 | await dbManage.pubArticle({ 38 | name: art.name, 39 | author: art.author, 40 | translator: art.translator, 41 | note: art.note, 42 | content: art.content, 43 | source, 44 | uploadTime: art.uploadTime * 1000, 45 | tags: art.taggedList.map(tagged => id2Tag[tagged.tagId]), 46 | }); 47 | i++; 48 | if (i % 50 === 0) { 49 | logger.info(`transferred ${i} articles`); 50 | } 51 | } 52 | await dbManage.disconnect(); 53 | } 54 | 55 | task().then(() => logger.info('task done!')); 56 | -------------------------------------------------------------------------------- /tools/duplicateChecker.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { resolve } = require('path'); 3 | const MD5 = new (require('jshashes').MD5)(); 4 | const logger = require('log4js').getLogger('checker'); 5 | const { path, duplicateKey } = require('./config'); 6 | const { formatTimeStamp } = require('../src/renderer/utils/renderer-utils'); 7 | 8 | let { duplicateFilter } = require('./config'); 9 | 10 | logger.level = 'info'; 11 | 12 | logger.info(`check duplicate contents on article.${duplicateKey} in ${path}`); 13 | 14 | const prisma = new PrismaClient({ 15 | datasources: { db: { url: `file:${resolve(path)}` } }, 16 | }); 17 | 18 | async function task() { 19 | const duplicateDict = {}; 20 | const hashSrcDict = {}; 21 | ( 22 | await prisma.article.findMany({ 23 | select: { 24 | id: true, 25 | name: true, 26 | content: true, 27 | source: true, 28 | uploadTime: true, 29 | }, 30 | orderBy: { 31 | uploadTime: 'asc', 32 | }, 33 | }) 34 | ).forEach(art => { 35 | let hashSrcArr = [art[duplicateKey]]; 36 | if (duplicateKey === 'source') { 37 | hashSrcArr = art[duplicateKey] 38 | .split(' ') 39 | .map(v => `${v}${v.startsWith('http') && !v.endsWith('/') ? '/' : ''}`); 40 | } 41 | for (const hashSrc of hashSrcArr) { 42 | const hash = MD5.hex(hashSrc); 43 | if (!duplicateDict[hash]) { 44 | duplicateDict[hash] = []; 45 | hashSrcDict[hash] = hashSrc; 46 | } 47 | duplicateDict[hash].push({ 48 | id: art.id, 49 | name: art.name, 50 | content: MD5.hex(art.content), 51 | source: art.source, 52 | uploadTime: art.uploadTime, 53 | }); 54 | } 55 | }); 56 | duplicateFilter = duplicateFilter 57 | .map(v => `${v}${v.startsWith('http') && !v.endsWith('/') ? '/' : ''}`) 58 | .sort(); 59 | const outDict = Object.keys(duplicateDict); 60 | outDict.sort((a, b) => { 61 | const sA = hashSrcDict[a], 62 | sB = hashSrcDict[b]; 63 | const dA = duplicateDict[a].length, 64 | dB = duplicateDict[b].length; 65 | const fA = duplicateFilter.indexOf(sA), 66 | fB = duplicateFilter.indexOf(sB); 67 | if (fA > -1 || fB > -1) { 68 | return fA - fB; 69 | } 70 | if (dA === dB) { 71 | return sA > sB ? 1 : -1; 72 | } 73 | return dB - dA; 74 | }); 75 | for (const key of outDict) { 76 | if ( 77 | duplicateDict[key].length > 1 && 78 | (duplicateKey !== 'source' || 79 | duplicateFilter.indexOf(hashSrcDict[key]) === -1) 80 | ) { 81 | logger.warn( 82 | `${key}${duplicateKey === 'source' ? '\t' + hashSrcDict[key] : ''}${ 83 | duplicateDict[key].length > 1 ? '\t' + duplicateDict[key].length : '' 84 | }`, 85 | ); 86 | duplicateDict[key].forEach(x => { 87 | logger.warn( 88 | `\t${x.id}\t${x.name}\t${x.content}${ 89 | duplicateKey === 'source' ? '' : '\t' + x.source 90 | }\t${formatTimeStamp(x.uploadTime * 1000)}`, 91 | ); 92 | }); 93 | } 94 | if ( 95 | duplicateKey === 'source' && 96 | duplicateFilter.indexOf(hashSrcDict[key]) !== -1 97 | ) { 98 | logger.info(`${key}\t${hashSrcDict[key]}\t${duplicateDict[key].length}`); 99 | } 100 | } 101 | await prisma.$disconnect(); 102 | } 103 | 104 | task().then(() => logger.info('task done!')); 105 | -------------------------------------------------------------------------------- /tools/metaTransfer.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { join, resolve } = require('path'); 3 | const { path, srcPath, transferringCreators } = require('./config.js'); 4 | const { transferringNovels } = require('./config'); 5 | const logger = require('log4js').getLogger('transfer'); 6 | logger.level = 'info'; 7 | 8 | logger.info(`transfer meta data from ${srcPath} into ${path}`); 9 | 10 | async function task() { 11 | let prisma = new PrismaClient({ 12 | datasources: { 13 | db: { 14 | url: `file:${join(resolve(srcPath))}`, 15 | }, 16 | }, 17 | }); 18 | const tagList = await prisma.tag.findMany({ 19 | orderBy: [{ type: 'asc' }, { name: 'asc' }], 20 | }); 21 | tagList.forEach(x => delete x.id); 22 | let creators = undefined; 23 | if (transferringCreators) { 24 | creators = await prisma.creator.findUnique({ 25 | where: { id: 1 }, 26 | }); 27 | } 28 | const dict = await prisma.dict.findMany(); 29 | const rec = await prisma['rec'].findMany(); 30 | await prisma.$disconnect(); 31 | prisma = new PrismaClient({ 32 | datasources: { 33 | db: { 34 | url: `file:${join(resolve(path))}`, 35 | }, 36 | }, 37 | }); 38 | if (transferringNovels) { 39 | for (const tag of tagList) { 40 | await prisma.tag.create({ 41 | data: tag, 42 | }); 43 | } 44 | } else { 45 | const tempList = tagList.filter(x => x.type !== 3); 46 | for (const tag of tempList) { 47 | await prisma.tag.create({ 48 | data: { 49 | name: tag.name, 50 | type: tag.type, 51 | }, 52 | }); 53 | } 54 | } 55 | logger.info(`transferred ${tagList.length} tags`); 56 | if (creators) { 57 | if ((await prisma.creator.count()) > 0) { 58 | await prisma.creator.update({ 59 | where: { 60 | id: 1, 61 | }, 62 | data: { 63 | names: creators.names, 64 | }, 65 | }); 66 | } else { 67 | await prisma.creator.create({ 68 | data: { 69 | names: creators.names, 70 | }, 71 | }); 72 | } 73 | logger.info('transferring creators done'); 74 | } 75 | for (const entry of dict) { 76 | await prisma.dict.create({ data: entry }); 77 | } 78 | for (const entry of rec) { 79 | await prisma['rec'].create({ data: entry }); 80 | } 81 | logger.info('transferring recommendation data done'); 82 | await prisma.$queryRaw`vacuum;`; 83 | await prisma.$disconnect(); 84 | } 85 | 86 | task().then(() => logger.info('task done!')); 87 | -------------------------------------------------------------------------------- /tools/recFixer.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { join, resolve } = require('path'); 3 | const { path } = require('./config.js'); 4 | const logger = require('log4js').getLogger('fixer'); 5 | logger.level = 'info'; 6 | 7 | const prisma = new PrismaClient({ 8 | datasources: { 9 | db: { 10 | url: `file:${join(resolve(path))}`, 11 | }, 12 | }, 13 | }); 14 | 15 | async function task() { 16 | const dict = await prisma.dict.findMany(); 17 | for (const entry of dict) { 18 | const data = {}; 19 | if (entry['refId']) { 20 | const key = entry.key.replace(/[【】]/g, ''); 21 | const ref = await prisma.tag.findFirst({ 22 | where: { name: key }, 23 | select: { 24 | id: true, 25 | }, 26 | }); 27 | if (!ref || !ref.id) { 28 | logger.error(entry.key); 29 | } else if (entry['refId'] !== ref.id) { 30 | logger.info(`fix: tag ${entry.key}, id ${entry['refId']} -> ${ref.id}`); 31 | data['refId'] = ref.id; 32 | } 33 | 34 | if (entry['relatedId']) { 35 | const art = await prisma.article.findFirst({ 36 | where: { name: entry.related }, 37 | select: { 38 | id: true, 39 | }, 40 | }); 41 | if (!art) { 42 | logger.error(entry.related); 43 | } else if (entry['relatedId'] !== art.id) { 44 | logger.info( 45 | `fix: article ${entry.related}, id ${entry['relatedId']} -> ${art.id}`, 46 | ); 47 | data['relatedId'] = art.id; 48 | } 49 | } 50 | 51 | if (Object.keys(data).length) { 52 | await prisma.dict.update({ where: { id: entry.id }, data }); 53 | } 54 | } 55 | } 56 | logger.info('fix dict done!'); 57 | 58 | const recs = await prisma['rec'].findMany(); 59 | for (const rec of recs) { 60 | const data = {}; 61 | switch (rec.type) { 62 | case 2: 63 | case 3: 64 | case 4: 65 | { 66 | const others = JSON.parse(rec.others || '{}'); 67 | const obj = await prisma.tag.findFirst({ 68 | where: { 69 | name: rec.title, 70 | }, 71 | select: { id: true, name: true }, 72 | }); 73 | if (!obj) { 74 | !others.join && logger.error(rec); 75 | } else if (rec.refId !== obj.id) { 76 | logger.info( 77 | `fix: tag ${rec.title}, id ${rec['refId']} -> ${obj.id}`, 78 | ); 79 | data.refId = obj.id; 80 | } 81 | const tags = await prisma.tag.findMany({ 82 | where: { 83 | name: { 84 | in: Object.values(others).filter(x => typeof x === 'string'), 85 | }, 86 | }, 87 | select: { id: true, name: true }, 88 | }); 89 | if (tags.length) { 90 | const newOthers = {}; 91 | tags.forEach(t => (newOthers[t.id] = t.name)); 92 | data.others = JSON.stringify(newOthers); 93 | if (data.others === rec.others) { 94 | delete data.others; 95 | } else { 96 | logger.info( 97 | `fix: tag ${rec.title}, others ${rec.others} -> ${data.others}`, 98 | ); 99 | } 100 | } 101 | } 102 | break; 103 | case 5: 104 | { 105 | const others = JSON.parse(rec.others || '{}'); 106 | const art = await prisma.article.findFirst({ 107 | where: { name: rec.title }, 108 | select: { id: true, name: true }, 109 | }); 110 | if (!art) { 111 | logger.error(rec); 112 | } else if (rec.refId !== art.id) { 113 | logger.info( 114 | `fix: article ${rec.title}, id ${rec['refId']} -> ${art.id}`, 115 | ); 116 | data.refId = art.id; 117 | } 118 | const articles = await prisma.article.findMany({ 119 | where: { 120 | name: { 121 | in: Object.values(others).filter(x => typeof x === 'string'), 122 | }, 123 | }, 124 | select: { id: true, name: true }, 125 | }); 126 | if (articles.length) { 127 | const newOthers = {}; 128 | articles.forEach(t => (newOthers[t.id] = t.name)); 129 | data.others = JSON.stringify(newOthers); 130 | if (data.others === rec.others) { 131 | delete data.others; 132 | } else { 133 | logger.info( 134 | `fix: article ${rec.title}, others ${rec.others} -> ${data.others}`, 135 | ); 136 | } 137 | } 138 | } 139 | break; 140 | default: 141 | break; 142 | } 143 | if (Object.keys(data).length) { 144 | await prisma['rec'].update({ 145 | where: { id: rec.id }, 146 | data, 147 | }); 148 | } 149 | } 150 | logger.info('fix rec done!'); 151 | } 152 | 153 | task().then(() => logger.info('task done!')); 154 | -------------------------------------------------------------------------------- /tools/result/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umalib/UmaLibDesktop/f6e58b5e6106b8490f3fd01b1ffc9e16680866b2/tools/result/.keep -------------------------------------------------------------------------------- /tools/result/paint_R15.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | from matplotlib import rcParams 7 | 8 | f = open('./output-pie.json', 'r', encoding='utf-8') 9 | data = json.loads(f.read()) 10 | f.close() 11 | 12 | config = { 13 | 'axes.unicode_minus': False, 14 | "figure.figsize": (16, 9), 15 | "font.family": ['Microsoft YaHei', 'msgothic', 'serif'], 16 | "font.size": 10, 17 | "mathtext.fontset": 'stix', 18 | } 19 | rcParams.update(config) 20 | 21 | R15 = data['R15'] 22 | 23 | keys = [] 24 | for i in range(0, len(R15['creators'])): 25 | keys.append('%s:%d(%.2f' % (R15['creators'][i], R15['counts'][i], R15['counts'][i] * 100 / R15['all']) + '%)') 26 | 27 | plt.pie(np.array(R15['counts']), labels=keys) 28 | plt.axis('equal') 29 | plt.title(u'R15创作饼图') 30 | plt.legend(loc="upper left") 31 | plt.savefig(u'./R15创作饼图.pdf') 32 | -------------------------------------------------------------------------------- /tools/result/paint_R18.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | from matplotlib import rcParams 7 | 8 | f = open('./output-pie.json', 'r', encoding='utf-8') 9 | data = json.loads(f.read()) 10 | f.close() 11 | 12 | config = { 13 | 'axes.unicode_minus': False, 14 | "figure.figsize": (16, 9), 15 | "font.family": ['Microsoft YaHei', 'msgothic', 'serif'], 16 | "font.size": 10, 17 | "mathtext.fontset": 'stix', 18 | } 19 | rcParams.update(config) 20 | 21 | R18 = data['R18'] 22 | 23 | keys = [] 24 | for i in range(0, len(R18['creators'])): 25 | keys.append('%s:%d(%.2f' % (R18['creators'][i], R18['counts'][i], R18['counts'][i] * 100 / R18['all']) + '%)') 26 | 27 | plt.pie(np.array(R18['counts']), labels=keys) 28 | plt.axis('equal') 29 | plt.title(u'R18创作饼图') 30 | plt.legend(loc="upper left") 31 | plt.savefig(u'./R18创作饼图.pdf') 32 | -------------------------------------------------------------------------------- /tools/result/plot_count_all.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import time 4 | 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | from matplotlib import rcParams 8 | 9 | f = open('./output-plot-count.json', 'r', encoding='utf-8') 10 | data = json.loads(f.read()) 11 | f.close() 12 | 13 | config = { 14 | 'axes.unicode_minus': False, 15 | "figure.figsize": (16, 9), 16 | "font.family": ['Microsoft YaHei', 'msgothic', 'serif'], 17 | "font.size": 10, 18 | "mathtext.fontset": 'stix', 19 | } 20 | rcParams.update(config) 21 | 22 | 23 | ticksX = [data['days'][0]] 24 | for t in range(18809,data['days'][-1] - 90): 25 | ticksName = time.strftime('%Y%m%d', time.localtime(t * 86400)) 26 | if ticksName.endswith('0101') or ticksName.endswith('0401') or ticksName.endswith('0701') or ticksName.endswith('1001'): 27 | ticksX.append(t) 28 | ticksX.append(data['days'][-1]) 29 | 30 | ticksY = [] 31 | for x in ticksX: 32 | ticksY.append(data['all'][data['days'].index(x)]) 33 | 34 | plt.plot(data['days'], data['all'], '-') 35 | plt.xticks(ticksX, map(lambda x: time.strftime('%Y%m%d', time.localtime(x * 86400))[2:], ticksX)) 36 | plt.yticks(list(set(ticksY))) 37 | plt.xlabel(u"时间") 38 | plt.ylabel(u"作品数") 39 | plt.grid(linestyle=":") 40 | plt.savefig(u'./作品总数增长曲线.pdf') 41 | -------------------------------------------------------------------------------- /tools/result/plot_count_creators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import math 4 | import time 5 | 6 | import matplotlib.pyplot as plt 7 | import numpy as np 8 | from matplotlib import rcParams 9 | 10 | f = open('./output-plot-count.json', 'r', encoding='utf-8') 11 | data = json.loads(f.read()) 12 | f.close() 13 | 14 | config = { 15 | 'axes.unicode_minus': False, 16 | "figure.figsize": (16, 9), 17 | "font.family": ['Microsoft YaHei', 'msgothic', 'serif'], 18 | "font.size": 10, 19 | "mathtext.fontset": 'stix', 20 | } 21 | rcParams.update(config) 22 | 23 | ticksX = [data['days'][0]] 24 | for t in range(18809,data['days'][-1] - 90): 25 | ticksName = time.strftime('%Y%m%d', time.localtime(t * 86400)) 26 | if ticksName.endswith('0101') or ticksName.endswith('0401') or ticksName.endswith('0701') or ticksName.endswith('1001'): 27 | ticksX.append(t) 28 | ticksX.append(data['days'][-1]) 29 | 30 | creatorLabels = data['dict']['creators'] 31 | 32 | ticksY = list(np.arange(0, data['creators'][0][-1], 20 if data['creators'][0][-1] < 300 else 50)) 33 | for i in range(0, len(creatorLabels)): 34 | ticksY.append(data['creators'][i][-1]) 35 | 36 | for i in range(0, len(creatorLabels)): 37 | plt.plot(data['days'], data['creators'][i], '-', label=creatorLabels[i]) 38 | plt.xticks(ticksX, map(lambda x: time.strftime('%Y%m%d', time.localtime(x * 86400))[2:], ticksX)) 39 | plt.yticks(list(set(ticksY))) 40 | plt.xlabel(u"时间") 41 | plt.ylabel(u"作品数") 42 | plt.legend(loc="best") 43 | plt.grid(linestyle=":") 44 | plt.savefig(u'./前10作者创作数增长曲线.pdf') 45 | -------------------------------------------------------------------------------- /tools/result/plot_count_delta.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import time 4 | 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | from matplotlib import rcParams 8 | 9 | f = open('./output-plot-count.json', 'r', encoding='utf-8') 10 | data = json.loads(f.read()) 11 | f.close() 12 | 13 | config = { 14 | 'axes.unicode_minus': False, 15 | "figure.figsize": (16, 9), 16 | "font.family": ['Microsoft YaHei', 'msgothic', 'serif'], 17 | "font.size": 10, 18 | "mathtext.fontset": 'stix', 19 | } 20 | rcParams.update(config) 21 | 22 | 23 | ticksX = [data['days'][0]] 24 | for t in range(18809,data['days'][-1] - 90): 25 | ticksName = time.strftime('%Y%m%d', time.localtime(t * 86400)) 26 | if ticksName.endswith('0101') or ticksName.endswith('0401') or ticksName.endswith('0701') or ticksName.endswith('1001'): 27 | ticksX.append(t) 28 | ticksX.append(data['days'][-1]) 29 | 30 | ticksY = list(np.arange(0, max(data['delta']), 10)) 31 | 32 | # exceptionVal = round(data['all'][-1] * 4 / len(data['all'])) 33 | # 34 | # for i in range(0, len(data['delta'])): 35 | # if data['delta'][i] > exceptionVal: 36 | # ticksX.append(data['days'][i]) 37 | # ticksY.append(data['delta'][i]) 38 | 39 | plt.bar(data['days'], data['delta']) 40 | plt.xticks(ticksX, map(lambda x: time.strftime('%Y%m%d', time.localtime(x * 86400))[2:], ticksX)) 41 | plt.yticks(list(set(ticksY))) 42 | plt.xlabel(u"时间") 43 | plt.ylabel(u"创作数") 44 | plt.grid(linestyle=":") 45 | plt.savefig(u'./作品增长量.pdf') 46 | -------------------------------------------------------------------------------- /tools/result/plot_count_tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import time 4 | 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | from matplotlib import rcParams 8 | 9 | f = open('./output-plot-count.json', 'r', encoding='utf-8') 10 | data = json.loads(f.read()) 11 | f.close() 12 | 13 | config = { 14 | 'axes.unicode_minus': False, 15 | "figure.figsize": (16, 9), 16 | "font.family": ['Microsoft YaHei', 'msgothic', 'serif'], 17 | "font.size": 10, 18 | "mathtext.fontset": 'stix', 19 | } 20 | rcParams.update(config) 21 | 22 | ticksX = [data['days'][0]] 23 | for t in range(18809,data['days'][-1] - 90): 24 | ticksName = time.strftime('%Y%m%d', time.localtime(t * 86400)) 25 | if ticksName.endswith('0101') or ticksName.endswith('0401') or ticksName.endswith('0701') or ticksName.endswith('1001'): 26 | ticksX.append(t) 27 | ticksX.append(data['days'][-1]) 28 | 29 | tagLabels = data['dict']['tags'] 30 | 31 | lineColor = ['red', 'orange', 'goldenrod', 'green', 'darkcyan', 'blue', 'purple', 'darkred', 'darkorange', 'olive', 32 | 'black', 'steelblue'] 33 | 34 | ticksY = list(np.arange(0, data['tags'][0][-1], 25 if data['tags'][0][-1] < 200 else 50)) 35 | for i in range(0, len(tagLabels)): 36 | ticksY.append(data['tags'][i][-1]) 37 | 38 | for i in range(0, len(tagLabels)): 39 | plt.plot(data['days'], data['tags'][i], '-', c=lineColor[i], label=tagLabels[i]) 40 | plt.xticks(ticksX, map(lambda x: time.strftime('%Y%m%d', time.localtime(x * 86400))[2:], ticksX)) 41 | plt.yticks(list(set(ticksY))) 42 | plt.xlabel(u"时间") 43 | plt.ylabel(u"作品数") 44 | plt.legend(loc="best") 45 | plt.grid(linestyle=":") 46 | plt.savefig(u'./前10标签及R18R15创作数增长曲线.pdf') 47 | -------------------------------------------------------------------------------- /tools/result/plot_len_all.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import time 4 | 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | from matplotlib import rcParams 8 | 9 | f = open('./output-plot-len.json', 'r', encoding='utf-8') 10 | data = json.loads(f.read()) 11 | f.close() 12 | 13 | config = { 14 | 'axes.unicode_minus': False, 15 | "figure.figsize": (16, 9), 16 | "font.family": ['Microsoft YaHei', 'msgothic', 'serif'], 17 | "font.size": 10, 18 | "mathtext.fontset": 'stix', 19 | } 20 | rcParams.update(config) 21 | 22 | ticksX = [data['days'][0]] 23 | for t in range(18809,data['days'][-1] - 90): 24 | ticksName = time.strftime('%Y%m%d', time.localtime(t * 86400)) 25 | if ticksName.endswith('0101') or ticksName.endswith('0401') or ticksName.endswith('0701') or ticksName.endswith('1001'): 26 | ticksX.append(t) 27 | ticksX.append(data['days'][-1]) 28 | 29 | for i in range(0, len(data['all'])): 30 | data['all'][i] /= 1000 31 | 32 | ticksY = [] 33 | for x in ticksX: 34 | ticksY.append(data['all'][data['days'].index(x)]) 35 | 36 | plt.plot(data['days'], data['all'], '-') 37 | plt.xticks(ticksX, map(lambda x: time.strftime('%Y%m%d', time.localtime(x * 86400))[2:], ticksX)) 38 | plt.yticks(list(set(ticksY))) 39 | plt.xlabel(u"时间") 40 | plt.ylabel(u"字符数(千字)") 41 | plt.grid(linestyle=":") 42 | plt.savefig(u'./作品字数增长曲线.pdf') 43 | -------------------------------------------------------------------------------- /tools/result/plot_len_creators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import time 4 | 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | from matplotlib import rcParams 8 | 9 | f = open('./output-plot-len.json', 'r', encoding='utf-8') 10 | data = json.loads(f.read()) 11 | f.close() 12 | 13 | config = { 14 | 'axes.unicode_minus': False, 15 | "figure.figsize": (16, 9), 16 | "font.family": ['Microsoft YaHei', 'msgothic', 'serif'], 17 | "font.size": 10, 18 | "mathtext.fontset": 'stix', 19 | } 20 | rcParams.update(config) 21 | 22 | ticksX = [data['days'][0], 18809, 18901, 18993, 19083, 19174, 19266, 19358, 19448, 19539, 19631, 19723,19814, 19905, data['days'][-1]] 23 | 24 | creatorLabels = data['dict']['creators'] 25 | 26 | for i in range(0, len(data['creators'])): 27 | for j in range(0, len(data['creators'][i])): 28 | data['creators'][i][j] /= 1000 29 | 30 | ticksY = list(np.arange(0, data['creators'][0][-1], 200)) 31 | for i in range(0, len(creatorLabels)): 32 | ticksY.append(data['creators'][i][-1]) 33 | 34 | for i in range(0, len(creatorLabels)): 35 | plt.plot(data['days'], data['creators'][i], '-', label=creatorLabels[i]) 36 | plt.xticks(ticksX, map(lambda x: time.strftime('%Y%m%d', time.localtime(x * 86400))[2:], ticksX)) 37 | plt.yticks(list(set(ticksY))) 38 | plt.xlabel(u"时间") 39 | plt.ylabel(u"字符数(千字)") 40 | plt.legend(loc="best") 41 | plt.grid(linestyle=":") 42 | plt.savefig(u'./前10作者字符数增长曲线.pdf') 43 | -------------------------------------------------------------------------------- /tools/result/plot_len_delta.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import time 4 | 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | from matplotlib import rcParams 8 | 9 | f = open('./output-plot-len.json', 'r', encoding='utf-8') 10 | data = json.loads(f.read()) 11 | f.close() 12 | 13 | config = { 14 | 'axes.unicode_minus': False, 15 | "figure.figsize": (16, 9), 16 | "font.family": ['Microsoft YaHei', 'msgothic', 'serif'], 17 | "font.size": 10, 18 | "mathtext.fontset": 'stix', 19 | } 20 | rcParams.update(config) 21 | 22 | ticksX = [data['days'][0]] 23 | for t in range(18809,data['days'][-1] - 90): 24 | ticksName = time.strftime('%Y%m%d', time.localtime(t * 86400)) 25 | if ticksName.endswith('0101') or ticksName.endswith('0401') or ticksName.endswith('0701') or ticksName.endswith('1001'): 26 | ticksX.append(t) 27 | ticksX.append(data['days'][-1]) 28 | 29 | maxY = max(data['delta']) / 1000 30 | 31 | for i in range(0, len(data['delta'])): 32 | data['delta'][i] /= 1000 33 | # if data['delta'][i] == maxY: 34 | # ticksX.append(data['days'][i]) 35 | 36 | maxY = round(maxY) 37 | 38 | ticksY = list(np.arange(0, maxY, 25)) 39 | ticksY.append(maxY) 40 | 41 | plt.bar(data['days'], data['delta']) 42 | plt.xticks(ticksX, map(lambda x: time.strftime('%Y%m%d', time.localtime(x * 86400))[2:], ticksX)) 43 | plt.yticks(list(set(ticksY))) 44 | plt.xlabel(u"时间") 45 | plt.ylabel(u"字符数(千字)") 46 | plt.grid(linestyle=":") 47 | plt.savefig(u'./作品字数增长量.pdf') 48 | -------------------------------------------------------------------------------- /tools/result/plot_len_tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import time 4 | 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | from matplotlib import rcParams 8 | 9 | f = open('./output-plot-len.json', 'r', encoding='utf-8') 10 | data = json.loads(f.read()) 11 | f.close() 12 | 13 | config = { 14 | 'axes.unicode_minus': False, 15 | "figure.figsize": (16, 9), 16 | "font.family": ['Microsoft YaHei', 'msgothic', 'serif'], 17 | "font.size": 10, 18 | "mathtext.fontset": 'stix', 19 | } 20 | rcParams.update(config) 21 | 22 | ticksX = [data['days'][0]] 23 | for t in range(18809,data['days'][-1] - 90): 24 | ticksName = time.strftime('%Y%m%d', time.localtime(t * 86400)) 25 | if ticksName.endswith('0101') or ticksName.endswith('0401') or ticksName.endswith('0701') or ticksName.endswith('1001'): 26 | ticksX.append(t) 27 | ticksX.append(data['days'][-1]) 28 | 29 | tagLabels = data['dict']['tags'] 30 | 31 | lineColor = ['red', 'orange', 'goldenrod', 'green', 'darkcyan', 'blue', 'purple', 'darkred', 'darkorange', 'olive', 32 | 'black', 'steelblue'] 33 | 34 | for i in range(0, len(data['tags'])): 35 | for j in range(0, len(data['tags'][i])): 36 | data['tags'][i][j] /= 1000 37 | 38 | ticksY = list(np.arange(0, data['tags'][10][-1], 200)) 39 | for i in range(0, len(tagLabels)): 40 | ticksY.append(data['tags'][i][-1]) 41 | 42 | for i in range(0, len(tagLabels)): 43 | plt.plot(data['days'], data['tags'][i], '-', c=lineColor[i], label=tagLabels[i]) 44 | plt.xticks(ticksX, map(lambda x: time.strftime('%Y%m%d', time.localtime(x * 86400))[2:], ticksX)) 45 | plt.yticks(list(set(ticksY))) 46 | plt.xlabel(u"时间") 47 | plt.ylabel(u"字符数(千字)") 48 | plt.legend(loc="best") 49 | plt.grid(linestyle=":") 50 | plt.savefig(u'./前10标签及双R字符数增长曲线.pdf') 51 | -------------------------------------------------------------------------------- /tools/signGenerator.js: -------------------------------------------------------------------------------- 1 | const { sm2 } = require('sm-crypto'); 2 | const { keyPair } = require('./config'); 3 | const logger = require('log4js').getLogger('generator'); 4 | const embeddedData = require('../src/renderer/utils/data'); 5 | logger.level = 'info'; 6 | 7 | const msg = embeddedData.signInfo.content; 8 | logger.info(msg); 9 | logger.info(keyPair.publicKey); 10 | logger.info( 11 | sm2.doSignature(msg, keyPair.privateKey, { 12 | der: true, 13 | hash: true, 14 | publicKey: keyPair.publicKey, 15 | }), 16 | ); 17 | logger.info( 18 | sm2.doSignature(msg, keyPair.privateKey, { 19 | der: true, 20 | hash: true, 21 | publicKey: keyPair.publicKey, 22 | }), 23 | ); 24 | -------------------------------------------------------------------------------- /tools/sourceChecker.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { join, resolve } = require('path'); 3 | const { path } = require('./config.js'); 4 | const logger = require('log4js').getLogger('checker'); 5 | logger.level = 'info'; 6 | 7 | logger.info(`check sources from ${path}`); 8 | 9 | const prisma = new PrismaClient({ 10 | datasources: { 11 | db: { 12 | url: `file:${join(resolve(path))}`, 13 | }, 14 | }, 15 | }); 16 | 17 | prisma.article.findMany().then(async r => { 18 | const dict = {}; 19 | for (const art of r) { 20 | for (const src of (art.source || '').split(' ')) { 21 | if (!dict[src]) { 22 | dict[src] = 1; 23 | } else { 24 | dict[src] += 1; 25 | } 26 | } 27 | } 28 | const outDict = Object.keys(dict).map(x => { 29 | return { k: x, v: dict[x] }; 30 | }); 31 | outDict.sort((a, b) => (a.k > b.k ? 1 : -1)); 32 | for (const entry of outDict) { 33 | if (entry.k && entry.v > 1) { 34 | logger.info(entry.k, entry.v); 35 | } 36 | } 37 | await prisma.$disconnect(); 38 | }); 39 | -------------------------------------------------------------------------------- /tools/tagAnalyser.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { writeFileSync } = require('fs'); 3 | const { join, resolve } = require('path'); 4 | const { path } = require('./config.js'); 5 | const logger = require('log4js').getLogger('analyser'); 6 | logger.level = 'info'; 7 | 8 | logger.info(`analyze article data in db ${path}`); 9 | 10 | function comparator(a, b) { 11 | if (a.count === b.count) { 12 | return a.name > b.name ? 1 : -1; 13 | } 14 | return b.count - a.count; 15 | } 16 | 17 | function stringifyCreators(sta) { 18 | const creators = sta.creators; 19 | const temp = []; 20 | for (const k in creators) { 21 | temp.push({ 22 | creator: k, 23 | count: creators[k], 24 | }); 25 | } 26 | temp.sort((a, b) => { 27 | if (b.count === a.count) { 28 | return a.creator > b.creator ? 1 : -1; 29 | } 30 | return b.count - a.count; 31 | }); 32 | sta.creators = temp; 33 | return temp.map(x => `${x.creator}(${x.count})`).join(' '); 34 | } 35 | 36 | function print(x, cb) { 37 | cb( 38 | `${x.name},${x.count}${ 39 | x.creators ? '/' + x.all + ',' + stringifyCreators(x) : '' 40 | }`, 41 | ); 42 | } 43 | 44 | const prisma = new PrismaClient({ 45 | datasources: { 46 | db: { 47 | url: `file:${join(resolve(path))}`, 48 | }, 49 | }, 50 | }); 51 | 52 | async function task() { 53 | const id2creator = {}; 54 | const creatorCount = {}; 55 | ( 56 | await prisma.article.findMany({ 57 | select: { 58 | id: true, 59 | author: true, 60 | translator: true, 61 | }, 62 | }) 63 | ).forEach(x => { 64 | const creator = x.translator ? x.translator : x.author; 65 | id2creator[x.id] = creator.split('/'); 66 | for (const c of id2creator[x.id]) { 67 | if (creatorCount[c] === undefined) { 68 | creatorCount[c] = 1; 69 | } else { 70 | creatorCount[c] += 1; 71 | } 72 | } 73 | }); 74 | const tags = await prisma.tag.findMany({ 75 | include: { 76 | taggedList: true, 77 | }, 78 | }); 79 | const longNovels = {}; 80 | tags 81 | .filter(x => x.type === 3) 82 | .forEach(x => 83 | x.taggedList.forEach(tagged => (longNovels[tagged.artId] = true)), 84 | ); 85 | const characters = [], 86 | series = [], 87 | others = []; 88 | const r18arts = {}, 89 | r15arts = {}; 90 | tags 91 | .filter(x => x.name === 'R18')[0] 92 | .taggedList.forEach(x => (r18arts[x.artId] = true)); 93 | tags 94 | .filter(x => x.name === 'R15')[0] 95 | .taggedList.forEach(x => (r15arts[x.artId] = true)); 96 | tags 97 | .filter(x => x.type === 1) 98 | .forEach(x => { 99 | let count = 0; 100 | const creators = {}; 101 | x.taggedList 102 | .filter(tagged => !longNovels[tagged.artId]) 103 | .forEach(tagged => { 104 | count++; 105 | id2creator[tagged.artId].forEach(creator => { 106 | if (!creators[creator]) { 107 | creators[creator] = 1; 108 | } else { 109 | creators[creator]++; 110 | } 111 | }); 112 | let tag = 'R18'; 113 | if (r18arts[tagged.artId]) { 114 | if (!creators[tag]) { 115 | creators[tag] = 1; 116 | } else { 117 | creators[tag]++; 118 | } 119 | } 120 | tag = 'R15'; 121 | if (r15arts[tagged.artId]) { 122 | if (!creators[tag]) { 123 | creators[tag] = 1; 124 | } else { 125 | creators[tag]++; 126 | } 127 | } 128 | }); 129 | characters.push({ 130 | name: x.name, 131 | count, 132 | all: x.taggedList.length, 133 | creators, 134 | }); 135 | }); 136 | characters.sort(comparator); 137 | tags 138 | .filter(x => x.type === 2) 139 | .forEach(x => series.push({ name: x.name, count: x.taggedList.length })); 140 | series.sort(comparator); 141 | tags 142 | .filter(x => !x.type) 143 | .forEach(x => { 144 | let creators = undefined; 145 | if (x.name === 'R18' || x.name === 'R15') { 146 | creators = {}; 147 | x.taggedList.forEach(tagged => { 148 | id2creator[tagged.artId].forEach(creator => { 149 | if (!creators[creator]) { 150 | creators[creator] = 1; 151 | } else { 152 | creators[creator]++; 153 | } 154 | }); 155 | }); 156 | } 157 | others.push({ 158 | name: x.name, 159 | count: x.taggedList.length, 160 | all: x.taggedList.length, 161 | creators, 162 | }); 163 | }); 164 | others.sort(comparator); 165 | const creators = {}; 166 | characters.forEach(x => { 167 | if (x.creators) { 168 | for (const c in x.creators) { 169 | if (!creators[c]) { 170 | creators[c] = {}; 171 | } 172 | creators[c][x.name] = x.creators[c]; 173 | } 174 | } 175 | }); 176 | const creatorSta = []; 177 | for (const c in creators) { 178 | const tempList = []; 179 | for (const ch in creators[c]) { 180 | tempList.push({ name: ch, count: creators[c][ch] }); 181 | } 182 | creatorSta.push({ 183 | name: c, 184 | count: creatorCount[c] || tempList.reduce((p, c) => p + c.count, 0), 185 | val: tempList.sort((a, b) => { 186 | if (a.count === b.count) { 187 | return a.name > b.name ? 1 : -1; 188 | } 189 | return b.count - a.count; 190 | }), 191 | }); 192 | } 193 | creatorSta.sort((a, b) => { 194 | if (a.count === b.count) { 195 | return a.name > b.name ? 1 : -1; 196 | } 197 | return b.count - a.count; 198 | }); 199 | 200 | // const cb = console.log; 201 | // characters.forEach(x => print(x, cb)); 202 | // cb(); 203 | // series.forEach(x => print(x, cb)); 204 | // cb(); 205 | // others.forEach(x => print(x, cb)); 206 | 207 | let output = ''; 208 | 209 | function cb(s) { 210 | output += s + '\n'; 211 | } 212 | 213 | cb('Character,Short/All,Creators'); 214 | characters.forEach(x => print(x, cb)); 215 | cb('\nSeries,Count'); 216 | series.forEach(x => print(x, cb)); 217 | cb('\nTag,Count'); 218 | others.forEach(x => print(x, cb)); 219 | cb('\nCreator,Count,Tags'); 220 | cb( 221 | creatorSta 222 | .map( 223 | x => 224 | `${x.name},${x.count},${x.val 225 | .map(v => `${v.name}(${v.count})`) 226 | .join(' ')}`, 227 | ) 228 | .join('\n'), 229 | ); 230 | cb('\nCharacter,R18,Count,Ratio'); 231 | characters 232 | .map(x => { 233 | let r18 = x.creators.filter(x => x.creator === 'R18'); 234 | if (r18.length) { 235 | r18 = r18[0].count; 236 | } else { 237 | r18 = 0; 238 | } 239 | return { 240 | name: x.name, 241 | count: x.count, 242 | r18, 243 | ratio: ((r18 || 0) * 100) / x.count, 244 | }; 245 | }) 246 | .filter(x => x.r18) 247 | .sort((a, b) => b.ratio - a.ratio) 248 | .forEach(x => { 249 | cb(`${x.name},${x.r18},${x.count},${x.ratio.toFixed(2)}%`); 250 | }); 251 | cb('\nCharacter,R15,Count,Ratio'); 252 | characters 253 | .map(x => { 254 | let r15 = x.creators.filter(x => x.creator === 'R15'); 255 | if (r15.length) { 256 | r15 = r15[0].count; 257 | } else { 258 | r15 = 0; 259 | } 260 | return { 261 | name: x.name, 262 | count: x.count, 263 | r15, 264 | ratio: ((r15 || 0) * 100) / x.count, 265 | }; 266 | }) 267 | .filter(x => x.r15) 268 | .sort((a, b) => b.ratio - a.ratio) 269 | .forEach(x => { 270 | cb(`${x.name},${x.r15},${x.count},${x.ratio.toFixed(2)}%`); 271 | }); 272 | const date = new Date(); 273 | const month = date.getMonth() + 1; 274 | const day = date.getDate(); 275 | writeFileSync( 276 | `result/analysis-${date.getFullYear()}${month < 10 ? '0' + month : month}${ 277 | day < 10 ? '0' + day : day 278 | }.csv`, 279 | `\uFEFF${output}`, 280 | ); 281 | const R18TagSta = others.filter(x => x.name === 'R18')[0], 282 | R15TagSta = others.filter(x => x.name === 'R15')[0]; 283 | writeFileSync( 284 | 'result/output-pie.json', 285 | JSON.stringify({ 286 | R18: { 287 | all: R18TagSta.count, 288 | creators: R18TagSta.creators.map(x => x.creator), 289 | counts: R18TagSta.creators.map(x => x.count), 290 | }, 291 | R15: { 292 | all: R15TagSta.count, 293 | creators: R15TagSta.creators.map(x => x.creator), 294 | counts: R15TagSta.creators.map(x => x.count), 295 | }, 296 | }), 297 | ); 298 | await prisma.$disconnect(); 299 | } 300 | 301 | task().then(() => logger.info('task done!')); 302 | -------------------------------------------------------------------------------- /tools/translatorReward.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | const { writeFileSync } = require('fs'); 3 | const { resolve } = require('path'); 4 | const { path } = require('./config.js'); 5 | const logger = require('log4js').getLogger('reward'); 6 | logger.level = 'info'; 7 | 8 | logger.info(`analyze article and translator data in db ${path}`); 9 | 10 | const prisma = new PrismaClient({ 11 | datasources: { 12 | db: { 13 | url: `file:${resolve(path)}`, 14 | }, 15 | }, 16 | }); 17 | 18 | async function task() { 19 | const removedArtDict = {}; 20 | ( 21 | await prisma.tagged.findMany({ 22 | where: { 23 | OR: [ 24 | { 25 | tag: { 26 | name: { 27 | in: ['R18', '安科', 'AA'], 28 | }, 29 | }, 30 | }, 31 | { 32 | tag: { 33 | type: { 34 | in: [3, 4], 35 | }, 36 | }, 37 | }, 38 | ], 39 | }, 40 | include: { 41 | tag: true, 42 | }, 43 | }) 44 | ).forEach(e => (removedArtDict[e.artId] = true)); 45 | console.log(`unselect ${Object.keys(removedArtDict).length} articles`); 46 | const artList = ( 47 | await prisma.article.findMany({ 48 | where: { 49 | translator: { 50 | not: '', 51 | }, 52 | }, 53 | select: { 54 | id: true, 55 | content: true, 56 | name: true, 57 | translator: true, 58 | }, 59 | }) 60 | ) 61 | .filter(e => !removedArtDict[e.id]) 62 | .map(e => { 63 | return { 64 | translator: e.translator, 65 | title: e.name, 66 | wordCounts: e.content 67 | .replace(/<[^>]*>/g, '') 68 | .replace( 69 | /[。?!,、;:“”‘’「」()[\]〔〕【】『』—…·~~《》〈〉_/.?!,;:"'<>()@#$%^&*+=\\`\s]/g, 70 | '', 71 | ).length, 72 | }; 73 | }) 74 | .filter(e => e.wordCounts > 0); 75 | 76 | const cdfData = {}; 77 | artList.forEach( 78 | e => (cdfData[e.wordCounts] = (cdfData[e.wordCounts] || 0) + 1), 79 | ); 80 | writeFileSync('./result/transCDF.json', JSON.stringify(cdfData)); 81 | 82 | const transDict = {}; 83 | artList.forEach(e => { 84 | if (!transDict[e.translator]) { 85 | transDict[e.translator] = { short: 0, medium: 0, long: 0, len: 0 }; 86 | } 87 | if (e.wordCounts >= 5000) { 88 | transDict[e.translator].long++; 89 | transDict[e.translator].len += e.wordCounts; 90 | } else if (e.wordCounts >= 2000) { 91 | transDict[e.translator].medium++; 92 | transDict[e.translator].len += e.wordCounts; 93 | } else if (e.wordCounts >= 300) { 94 | transDict[e.translator].short++; 95 | transDict[e.translator].len += e.wordCounts; 96 | } 97 | }); 98 | 99 | const buffer = Object.keys(transDict) 100 | .map(e => { 101 | transDict[e].translator = e; 102 | return transDict[e]; 103 | }) 104 | .filter(e => e.len) 105 | .sort((a, b) => { 106 | if (a.len === b.len) { 107 | if (b.long === a.long) { 108 | if (b.medium === a.medium) { 109 | if (b.short === a.short) { 110 | return a.translator > b.translator ? 1 : -1; 111 | } 112 | return b.short - a.short; 113 | } 114 | return b.medium - a.medium; 115 | } 116 | return b.long - a.long; 117 | } 118 | return b.len - a.len; 119 | }) 120 | .map(e => { 121 | const all = e.long + e.medium + e.short; 122 | return `${e.translator},${all},${e.len},${e.long},${e.medium},${ 123 | e.short 124 | },${(e.len / all).toFixed(2)}`; 125 | }) 126 | .join('\n'); 127 | writeFileSync( 128 | './result/reward.csv', 129 | `\ufeffTranslator,All,WordCount,Long,Medium,Short,AverageWordCount\n${buffer}`, 130 | ); 131 | 132 | await prisma.$disconnect(); 133 | } 134 | 135 | task().then(() => logger.info('task done!')); 136 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | port: 58090, 4 | }, 5 | pluginOptions: { 6 | electronBuilder: { 7 | builderOptions: { 8 | appId: 'cn.umafan.lib', 9 | productName: '赛马娘同人集中楼大书库', 10 | mac: { 11 | icon: 'icons/icon.icns', 12 | artifactName: 'umalib-mac-v${version}.${ext}', 13 | target: ['dmg'], 14 | }, 15 | win: { 16 | icon: 'icons/256x256.png', 17 | artifactName: 'umalib-win64-v${version}.${ext}', 18 | target: [{ target: '7z', arch: ['x64'] }], 19 | }, 20 | extraResources: [ 21 | 'prisma/**/*', 22 | 'node_modules/.prisma/**/*', 23 | 'node_modules/@prisma/client/**/*', 24 | ], 25 | }, 26 | externals: ['@prisma/client'], 27 | nodeIntegration: true, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /web-types.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/web-types", 3 | "framework": "vue", 4 | "name": "name written in package.json", 5 | "version": "version written in package.json", 6 | "contributions": { 7 | "html": { 8 | "types-syntax": "typescript", 9 | "attributes": [ 10 | { 11 | "name": "v-loading" 12 | } 13 | ] 14 | } 15 | } 16 | } --------------------------------------------------------------------------------