├── .gitignore ├── docs ├── Home.png ├── logo.png ├── Profile.png ├── StarList.png ├── TagList.png ├── SeriesList.png ├── are-you-18.jpg ├── BookmarkInfo.png ├── BookmarkList.png ├── MetadataList.png ├── MetadataInfoTop.png └── MetadataInfoBottom.png ├── src ├── module │ ├── config.js │ ├── logger.js │ ├── database.js │ ├── statistic.js │ ├── ignore.js │ ├── cache.js │ ├── stack.js │ ├── announcement.js │ ├── migration.js │ ├── permission.js │ ├── file.js │ ├── invitation.js │ ├── video.js │ ├── user.js │ ├── bookmark.js │ ├── driver │ │ └── googleDrive.js │ └── metadata.js ├── app.js ├── api │ ├── route │ │ ├── statistic.js │ │ ├── file.js │ │ ├── video.js │ │ ├── invitation.js │ │ ├── announcement.js │ │ ├── group.js │ │ ├── auth.js │ │ ├── metadata.js │ │ ├── bookmark.js │ │ └── user.js │ └── init.js └── import │ ├── init.js │ └── driver │ └── googleDrive.js ├── Dockerfile ├── .editorconfig ├── docker-compose.example.yml ├── .eslintrc.js ├── migrations ├── 20200725133104-index.js ├── 20200722083906-announcement.js ├── 20200721022852-usersystem.js └── 20200720090000-init.js ├── config └── dev.example.json ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | config/*.json 4 | src/test.js 5 | -------------------------------------------------------------------------------- /docs/Home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVClub/core/HEAD/docs/Home.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVClub/core/HEAD/docs/logo.png -------------------------------------------------------------------------------- /docs/Profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVClub/core/HEAD/docs/Profile.png -------------------------------------------------------------------------------- /docs/StarList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVClub/core/HEAD/docs/StarList.png -------------------------------------------------------------------------------- /docs/TagList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVClub/core/HEAD/docs/TagList.png -------------------------------------------------------------------------------- /docs/SeriesList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVClub/core/HEAD/docs/SeriesList.png -------------------------------------------------------------------------------- /docs/are-you-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVClub/core/HEAD/docs/are-you-18.jpg -------------------------------------------------------------------------------- /docs/BookmarkInfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVClub/core/HEAD/docs/BookmarkInfo.png -------------------------------------------------------------------------------- /docs/BookmarkList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVClub/core/HEAD/docs/BookmarkList.png -------------------------------------------------------------------------------- /docs/MetadataList.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVClub/core/HEAD/docs/MetadataList.png -------------------------------------------------------------------------------- /docs/MetadataInfoTop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVClub/core/HEAD/docs/MetadataInfoTop.png -------------------------------------------------------------------------------- /docs/MetadataInfoBottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JAVClub/core/HEAD/docs/MetadataInfoBottom.png -------------------------------------------------------------------------------- /src/module/config.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_CONFIG_DIR = process.cwd() + '/config/' 2 | const config = require('config') 3 | 4 | module.exports = config 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | WORKDIR /usr/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 3000 12 | CMD [ "node", "src/app.js" ] 13 | -------------------------------------------------------------------------------- /src/module/logger.js: -------------------------------------------------------------------------------- 1 | const log4js = require('log4js') 2 | const config = require('config') 3 | 4 | module.exports = (category) => { 5 | const logger = log4js.getLogger(category) 6 | logger.level = config.get('system.logLevel') 7 | 8 | return logger 9 | } 10 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | ;(async () => { 2 | const Sentry = require('@sentry/node') 3 | 4 | Sentry.init({ 5 | dsn: 'https://a5df6f6888404ec492be93b7e93b5dd3@o230009.ingest.sentry.io/5217379' 6 | }) 7 | })() 8 | 9 | require('./module/migration') 10 | require('./api/init') 11 | require('./import/init') 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/module/database.js: -------------------------------------------------------------------------------- 1 | const config = require('./config') 2 | const knex = require('knex')({ 3 | client: 'mysql', 4 | connection: { 5 | ...config.get('database'), 6 | user: config.get('database.username') 7 | } 8 | }) 9 | const { attachPaginate } = require('knex-paginate') 10 | attachPaginate() 11 | 12 | module.exports = knex 13 | -------------------------------------------------------------------------------- /docker-compose.example.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | 5 | core: 6 | image: javclub/core:latest 7 | volumes: 8 | - ./config:/usr/app/config 9 | environment: 10 | - NODE_ENV=dev 11 | - TZ=Asia/Shanghai 12 | restart: unless-stopped 13 | networks: 14 | - docker-lemp_default 15 | 16 | networks: 17 | docker-lemp_default: 18 | external: true 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | node: true, 5 | es2020: true 6 | }, 7 | extends: [ 8 | 'standard' 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 11 12 | }, 13 | rules: { 14 | indent: [ 15 | 'error', 16 | 2 17 | ], 18 | quotes: [ 19 | 'error', 20 | 'single' 21 | ], 22 | semi: [ 23 | 'error', 24 | 'never' 25 | ] 26 | }, 27 | plugins: [ 28 | 'markdown', 29 | 'standard' 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/api/route/statistic.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const cache = require('./../../module/cache') 4 | const statistic = require('./../../module/statistic') 5 | 6 | router.get('/getData', async (req, res) => { 7 | const result = await cache('api_statistic', async () => { 8 | const res = await statistic.getData() 9 | return res 10 | }, 10000) 11 | 12 | res.json({ 13 | code: 0, 14 | msg: 'Success', 15 | data: result 16 | }) 17 | }) 18 | 19 | module.exports = router 20 | -------------------------------------------------------------------------------- /src/module/statistic.js: -------------------------------------------------------------------------------- 1 | const db = require('./database') 2 | 3 | class Statistic { 4 | async getData () { 5 | const tableList = [ 6 | 'drivers', 7 | 'files', 8 | 'metadatas', 9 | 'series', 10 | 'stars', 11 | 'tags', 12 | 'videos' 13 | ] 14 | 15 | const result = {} 16 | for (const i in tableList) { 17 | const res = await db(tableList[i]).count() 18 | result[tableList[i]] = res[0]['count(*)'] 19 | } 20 | 21 | return result 22 | } 23 | } 24 | 25 | module.exports = new Statistic() 26 | -------------------------------------------------------------------------------- /src/module/ignore.js: -------------------------------------------------------------------------------- 1 | const logger = require('./../module/logger')('Module: ignore') 2 | const db = require('./database') 3 | 4 | class Ignore { 5 | async checkIgnoreStatus (data) { 6 | const result = await db('ignore').where('data', JSON.stringify(data)).count() 7 | 8 | if (result && result[0]['count(*)'] === 0) return false 9 | return true 10 | } 11 | 12 | async addIgnore (data) { 13 | const result = await db('ignore').insert({ 14 | data: JSON.stringify(data) 15 | }) 16 | 17 | logger.debug('Add', JSON.stringify(data), 'to ignore list') 18 | 19 | return result 20 | } 21 | } 22 | 23 | module.exports = new Ignore() 24 | -------------------------------------------------------------------------------- /migrations/20200725133104-index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | queryInterface.addIndex( 6 | 'videos', 7 | ['videoMetadata'], 8 | { 9 | indicesType: 'UNIQUE' 10 | } 11 | ) 12 | 13 | queryInterface.addIndex( 14 | 'files', 15 | ['driverId', 'storageData'], 16 | { 17 | indicesType: 'UNIQUE' 18 | } 19 | ) 20 | }, 21 | 22 | down: async (queryInterface, Sequelize) => { 23 | /** 24 | * Add reverting commands here. 25 | * 26 | * Example: 27 | * await queryInterface.dropTable('users'); 28 | */ 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/api/route/file.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const cache = require('./../../module/cache') 4 | const file = require('./../../module/file') 5 | const _ = require('lodash') 6 | 7 | router.get('/getURL/:str', async (req, res) => { 8 | if (!req.params.str) { 9 | return res.json({ 10 | code: -2, 11 | msg: 'Param error', 12 | data: {} 13 | }) 14 | } 15 | 16 | let str = req.params.str 17 | str = str.split(',') 18 | const arr = [] 19 | for (const i in str) arr.push(parseInt(str[i])) 20 | 21 | const result = await cache(`api_file_get_${str}`, file.getFilesURL(_.chunk(arr, 100)[0])) 22 | 23 | res.json({ 24 | code: 0, 25 | msg: 'Success', 26 | data: result 27 | }) 28 | }) 29 | 30 | module.exports = router 31 | -------------------------------------------------------------------------------- /src/module/cache.js: -------------------------------------------------------------------------------- 1 | const logger = require('./logger')('Module: Cache') 2 | const cachePool = {} 3 | 4 | setInterval(() => { 5 | for (const i in cachePool) { 6 | const item = cachePool[i] 7 | 8 | if (item && item.expireTime !== 0 && item.expireTime < (new Date()).getTime()) { 9 | delete cachePool[i] 10 | logger.debug(`Expired cache ${i} cleared`) 11 | } 12 | } 13 | }, 60000) 14 | 15 | module.exports = async (name, fn, time = 0) => { 16 | if (cachePool[name] && (cachePool[name].expireTime === 0 || cachePool[name].expireTime > (new Date()).getTime())) { 17 | return cachePool[name].value 18 | } 19 | 20 | logger.debug(`[${name}] Cache missed, creating one`) 21 | const result = await fn() 22 | cachePool[name] = { 23 | expireTime: (time === 0) ? 0 : (new Date()).getTime() + time, 24 | value: result 25 | } 26 | 27 | return result 28 | } 29 | -------------------------------------------------------------------------------- /migrations/20200722083906-announcement.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | queryInterface.createTable('announcements', { 6 | id: { 7 | type: Sequelize.TINYINT.UNSIGNED, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | allowNull: false, 11 | unique: true 12 | }, 13 | title: { 14 | type: Sequelize.TEXT, 15 | allowNull: false 16 | }, 17 | content: { 18 | type: Sequelize.TEXT, 19 | allowNull: false 20 | }, 21 | createTime: { 22 | type: Sequelize.STRING(20), 23 | allowNull: false 24 | }, 25 | updateTime: { 26 | type: Sequelize.STRING(20), 27 | allowNull: false 28 | } 29 | }) 30 | }, 31 | 32 | down: async (queryInterface, Sequelize) => { 33 | /** 34 | * Add reverting commands here. 35 | * 36 | * Example: 37 | * await queryInterface.dropTable('users'); 38 | */ 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/api/route/video.js: -------------------------------------------------------------------------------- 1 | const logger = require('./../../module/logger')('API: Video') 2 | const express = require('express') 3 | const router = express.Router() 4 | const cache = require('./../../module/cache') 5 | const video = require('./../../module/video') 6 | 7 | router.get('/getList/:metadataId/:page?/:size?', async (req, res) => { 8 | let { metadataId, page, size } = req.params 9 | metadataId = parseInt(metadataId || 1) 10 | page = parseInt(page || 1) 11 | size = parseInt(size || 20) 12 | logger.debug(`Metadata id ${metadataId}, page ${page}, size ${size}`) 13 | 14 | if (metadataId < 1 || page < 1 || size < 1) { 15 | res.json({ 16 | code: -2, 17 | msg: 'Param error', 18 | data: {} 19 | }) 20 | return 21 | } 22 | 23 | const result = await cache(`api_video_list_${metadataId}_${page}_${size}`, async () => { 24 | const res = await video.getVideoList(page, size, false, metadataId) 25 | return res 26 | }, 60000) 27 | 28 | res.json({ 29 | code: 0, 30 | msg: 'Success', 31 | data: result 32 | }) 33 | }) 34 | 35 | module.exports = router 36 | -------------------------------------------------------------------------------- /config/dev.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "system": { 3 | "logLevel": "debug", 4 | "port": 3000, 5 | "path": "/api", 6 | "allowChangeUsername": false, 7 | "userMaxBookmarkNum": 10, 8 | "userMaxBookmarkItemNum": 100, 9 | "corsDomain": [ 10 | "https://yourdomain.com" 11 | ], 12 | "searchParmaNum": 3, 13 | "allowSignup": false, 14 | "defaultGroup": 2 15 | }, 16 | "database": { 17 | "dialect": "mysql", 18 | "connectionLimit": 5, 19 | "host": "mysql", 20 | "port": 3306, 21 | "username": "javclub", 22 | "password": "javclub", 23 | "database": "javclub" 24 | }, 25 | "importer": { 26 | "settings": { 27 | "googleDrive": { 28 | "queueNum": 1 29 | } 30 | }, 31 | 32 | "cron": [ 33 | { 34 | "driveId": 1, 35 | "interval": 36000000, 36 | "doFull": true 37 | } 38 | ] 39 | }, 40 | "proxy": [ 41 | "https://proxy.xiaolin.in/" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) XiaoLin (https://cgl.li/) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/module/stack.js: -------------------------------------------------------------------------------- 1 | const logger = require('./logger')('Stack') 2 | const db = require('./database') 3 | const GDClass = require('./driver/googleDrive') 4 | 5 | class Stack { 6 | constructor () { 7 | this.instances = {} 8 | logger.info('Stack created') 9 | } 10 | 11 | /** 12 | * Get driver instance 13 | * 14 | * @param {Int} id Driver Id 15 | * @returns {Object} Driver Instance 16 | */ 17 | async getInstance (id) { 18 | if (this.instances[id]) return this.instances[id] 19 | 20 | logger.info('Creating Instance', id) 21 | const result = await db('drivers').where('isEnable', 1).where('id', id).first() 22 | if (result) { 23 | logger.debug(result) 24 | switch (result.driverType) { 25 | case 'gd': 26 | this.instances[id] = new GDClass(id, JSON.parse(result.driverData)) 27 | await this.instances[id].refreshToken() 28 | return this.instances[id] 29 | 30 | default: 31 | // DO NOTHING 32 | } 33 | } 34 | 35 | logger.error(`Driver ${id} not found`) 36 | throw new Error(`Driver ${id} not found`) 37 | } 38 | } 39 | 40 | module.exports = new Stack() 41 | -------------------------------------------------------------------------------- /src/api/route/invitation.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const invitation = require('./../../module/invitation') 4 | const permission = require('./../../module/permission') 5 | 6 | router.post('/createInvitation', async (req, res) => { 7 | const allow = await invitation.checkUserInvitationLimit(req.uid) 8 | 9 | if (allow === false) { 10 | res.json({ 11 | code: -2, 12 | msg: 'Limit exceeded', 13 | data: {} 14 | }) 15 | return 16 | } 17 | 18 | const result = await invitation.createInvitation(req.uid) 19 | 20 | res.json({ 21 | code: 0, 22 | msg: 'Success', 23 | data: { 24 | code: result 25 | } 26 | }) 27 | }) 28 | 29 | router.get('/getInvitationList/:page?/:size?', async (req, res) => { 30 | let { page, size } = req.params 31 | page = parseInt(page || 1) 32 | size = parseInt(size || 20) 33 | 34 | if (page < 1 || size < 1 || size > 50) { 35 | res.json({ 36 | code: -2, 37 | msg: 'Param error', 38 | data: {} 39 | }) 40 | return 41 | } 42 | 43 | let uid = req.uid 44 | const per = await permission.getUserPermissionGroupInfo(req.uid) 45 | if (per.rule.admin) uid = -1 46 | 47 | const result = await invitation.getUserInvitation(uid, page, size) 48 | 49 | res.json({ 50 | code: 0, 51 | msg: 'Success', 52 | data: { 53 | ...result 54 | } 55 | }) 56 | }) 57 | 58 | router.get('/getInvitationLimit', async (req, res) => { 59 | const result = await invitation.checkUserInvitationLimit(req.uid, false) 60 | 61 | res.json({ 62 | code: 0, 63 | msg: 'Success', 64 | data: { 65 | ...result 66 | } 67 | }) 68 | }) 69 | 70 | module.exports = router 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javclub_core", 3 | "version": "1.0.0", 4 | "description": "JAVClub core", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "npm run build", 8 | "dev": "NODE_ENV=dev node src/app.js", 9 | "run": "NODE_ENV=stage node dist/app.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/JAVClub/core.git" 14 | }, 15 | "keywords": [ 16 | "JAV" 17 | ], 18 | "author": "XiaoLin", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/JAVClub/core/issues" 22 | }, 23 | "homepage": "https://github.com/JAVClub/core#readme", 24 | "dependencies": { 25 | "@sentry/node": "^5.15.5", 26 | "bcrypt": "^4.0.1", 27 | "child_process": "^1.0.2", 28 | "config": "^3.3.0", 29 | "cookie-parser": "^1.4.5", 30 | "crypto-js": "^4.0.0", 31 | "dom-parser": "^0.1.6", 32 | "express": "^4.17.1", 33 | "google-auth-library": "3.1.2", 34 | "googleapis": "39.2.0", 35 | "googleapis-common": "0.7.2", 36 | "js-base64": "^2.5.2", 37 | "knex": "^0.20.11", 38 | "knex-paginate": "^1.2.0", 39 | "lodash": "^4.17.19", 40 | "log4js": "^6.1.2", 41 | "minimist": ">=1.2.2", 42 | "mysql": "^2.18.1", 43 | "mysql2": "^2.1.0", 44 | "node-fetch": "^2.6.0", 45 | "p-queue": "^6.3.0", 46 | "p-retry": "^4.2.0", 47 | "promise-queue-plus": "^1.2.2", 48 | "random-int": "^2.0.1", 49 | "randomstring": "^1.1.5", 50 | "sequelize": "^6.3.3", 51 | "sequelize-cli": "^6.2.0", 52 | "sha256": "^0.2.0" 53 | }, 54 | "devDependencies": { 55 | "eslint": "^7.3.1", 56 | "eslint-plugin-markdown": "^1.0.2", 57 | "pkg": "^4.4.9", 58 | "standard": "^14.3.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/import/init.js: -------------------------------------------------------------------------------- 1 | const randomInt = require('random-int') 2 | const logger = require('./../module/logger')('Importer: Main') 3 | const config = require('./../module/config') 4 | const stack = require('./../module/stack') 5 | const GDImporter = require('./driver/googleDrive') 6 | 7 | const startProcess = async (importerClass, doFull) => { 8 | try { 9 | await importerClass.run(doFull) 10 | } catch (error) { 11 | logger.error(error) 12 | } 13 | } 14 | 15 | const cron = config.get('importer.cron') 16 | logger.debug('Config:', cron) 17 | ;(async () => { 18 | for (const i in cron) { 19 | const item = cron[i] 20 | 21 | const instance = await stack.getInstance(item.driveId) 22 | const importerClass = new GDImporter(item.driveId, instance) 23 | 24 | const setCron = async () => { 25 | logger.debug(`[${item.driveId}] Cron set, ${item.interval}ms`) 26 | setTimeout(async () => { 27 | logger.info(`[${item.driveId}] Starting import process`) 28 | 29 | try { 30 | await startProcess(importerClass, false) 31 | } catch (e) { 32 | logger.error(`[${item.driveId}] Import process threw an error`, e) 33 | setCron() 34 | return 35 | } 36 | 37 | logger.info(`[${item.driveId}] Import process fininshed`) 38 | setCron() 39 | }, item.interval) 40 | } 41 | 42 | const queueTime = randomInt(10, 60) 43 | 44 | logger.info(`[${item.driveId}] Ready in ${queueTime} seconds`) 45 | 46 | setTimeout(async () => { 47 | const doFull = !!(item.doFull) 48 | logger.info(`[${item.driveId}] Starting first time import process`) 49 | 50 | try { 51 | await startProcess(importerClass, doFull) 52 | } catch (e) { 53 | logger.error(`[${item.driveId}] First time import process threw an error`, e) 54 | setCron() 55 | return 56 | } 57 | 58 | logger.info(`[${item.driveId}] First time import process fininshed`) 59 | setCron() 60 | }, queueTime * 1000) 61 | } 62 | })() 63 | -------------------------------------------------------------------------------- /src/module/announcement.js: -------------------------------------------------------------------------------- 1 | const db = require('./database') 2 | 3 | class Announcement { 4 | /** 5 | * Create announcement 6 | * 7 | * @param {String} title announcement title 8 | * @param {String} content announcement content 9 | * 10 | * @returns {Int} 11 | */ 12 | async createAnnouncement (title, content) { 13 | const result = await db('announcements').insert({ 14 | title, 15 | content, 16 | createTime: (new Date()).getTime(), 17 | updateTime: (new Date()).getTime() 18 | }) 19 | 20 | return result 21 | } 22 | 23 | /** 24 | * Change announcement 25 | * 26 | * @param {Int} id announcement id 27 | * @param {String} title announcement title 28 | * @param {String} content announcement content 29 | * 30 | * @returns {Int} 31 | */ 32 | async changeAnnouncement (id, title, content) { 33 | const result = await db('announcements').where('id', id).update({ 34 | title, 35 | content, 36 | updateTime: (new Date()).getTime() 37 | }) 38 | 39 | return result 40 | } 41 | 42 | /** 43 | * Remove announcement 44 | * 45 | * @param {Int} id announcement id 46 | * 47 | * @returns {Int} 48 | */ 49 | async removeAnnouncement (id) { 50 | const result = await db('announcements').where('id', id).delete() 51 | 52 | return result 53 | } 54 | 55 | /** 56 | * Get announcement list 57 | * 58 | * @param {Int=} page page number 59 | * @param {Int=} size page size 60 | * 61 | * @returns {Array} announcement list 62 | */ 63 | async getAnnouncementList (page, size) { 64 | const result = await db('announcements').orderBy('id', 'desc').select('*').paginate({ 65 | perPage: size, 66 | currentPage: page 67 | }) 68 | 69 | if (!result.data) { 70 | return { 71 | total: 0, 72 | data: [] 73 | } 74 | } 75 | 76 | let total = await db('announcements').count() 77 | total = total[0]['count(*)'] 78 | 79 | return { 80 | total, 81 | data: result.data 82 | } 83 | } 84 | } 85 | 86 | module.exports = new Announcement() 87 | -------------------------------------------------------------------------------- /migrations/20200721022852-usersystem.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | queryInterface.addColumn('users', 'from', { 6 | type: Sequelize.STRING(64), 7 | allowNull: false, 8 | defaultValue: 'direct' 9 | }) 10 | 11 | queryInterface.addColumn('users', 'permission_group', { 12 | type: Sequelize.TINYINT.UNSIGNED, 13 | allowNull: false, 14 | defaultValue: '2' 15 | }) 16 | 17 | queryInterface.createTable('invitations', { 18 | id: { 19 | type: Sequelize.INTEGER.UNSIGNED, 20 | primaryKey: true, 21 | autoIncrement: true, 22 | allowNull: false, 23 | unique: true 24 | }, 25 | creator: { 26 | type: Sequelize.INTEGER.UNSIGNED, 27 | allowNull: false 28 | }, 29 | code: { 30 | type: Sequelize.STRING(32), 31 | allowNull: false, 32 | unique: true 33 | }, 34 | useBy: { 35 | type: Sequelize.INTEGER.UNSIGNED, 36 | allowNull: true 37 | }, 38 | permission_group: { 39 | type: Sequelize.TINYINT.UNSIGNED, 40 | allowNull: false 41 | }, 42 | createTime: { 43 | type: Sequelize.STRING(20), 44 | allowNull: false 45 | }, 46 | useTime: { 47 | type: Sequelize.STRING(20), 48 | allowNull: true 49 | } 50 | }) 51 | 52 | await queryInterface.createTable('permission_groups', { 53 | id: { 54 | type: Sequelize.TINYINT.UNSIGNED, 55 | primaryKey: true, 56 | autoIncrement: true, 57 | allowNull: false, 58 | unique: true 59 | }, 60 | name: { 61 | type: Sequelize.STRING(20), 62 | allowNull: false 63 | }, 64 | rule: { 65 | type: Sequelize.STRING(256), 66 | allowNull: false 67 | }, 68 | createTime: { 69 | type: Sequelize.STRING(20), 70 | allowNull: false 71 | }, 72 | updateTime: { 73 | type: Sequelize.STRING(20), 74 | allowNull: true 75 | } 76 | }) 77 | }, 78 | 79 | down: async (queryInterface, Sequelize) => { 80 | /** 81 | * Add reverting commands here. 82 | * 83 | * Example: 84 | * await queryInterface.dropTable('users'); 85 | */ 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/module/migration.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | const config = require('./config') 3 | const db = require('./database') 4 | const path = require('path') 5 | const bcrypt = require('bcrypt') 6 | const randomString = require('randomstring') 7 | const binfile = path.resolve(__dirname, '../../node_modules/.bin', 'sequelize') 8 | 9 | console.log(execSync(binfile + ' --config config/dev.json --env database db:create ' + config.get('database.database')).toString()) 10 | console.log(execSync(binfile + ' --config config/dev.json --env database db:migrate --debug').toString()) 11 | 12 | module.exports = (async () => { 13 | if ((await db('permission_groups').count('*'))[0]['count(*)'] === 0) { 14 | await db('permission_groups').insert({ 15 | id: 1, 16 | name: 'Admin Group', 17 | rule: JSON.stringify({ 18 | admin: true, 19 | invitationNum: -1, 20 | invitationGroup: 2, 21 | title: 'Admin', 22 | banned: false 23 | }), 24 | createTime: (new Date()).getTime(), 25 | updateTime: (new Date()).getTime() 26 | }) 27 | 28 | await db('permission_groups').insert({ 29 | id: 2, 30 | name: 'User Group', 31 | rule: JSON.stringify({ 32 | admin: false, 33 | invitationNum: 1, 34 | invitationGroup: 2, 35 | title: 'User', 36 | banned: false 37 | }), 38 | createTime: (new Date()).getTime(), 39 | updateTime: (new Date()).getTime() 40 | }) 41 | 42 | await db('permission_groups').insert({ 43 | id: 3, 44 | name: 'Banned Group', 45 | rule: JSON.stringify({ 46 | admin: false, 47 | invitationNum: 0, 48 | invitationGroup: 3, 49 | title: 'Banned', 50 | banned: true 51 | }), 52 | createTime: (new Date()).getTime(), 53 | updateTime: (new Date()).getTime() 54 | }) 55 | } 56 | 57 | if ((await db('users').count('*'))[0]['count(*)'] === 0) { 58 | await db('users').insert({ 59 | id: 1, 60 | username: 'admin', 61 | password: bcrypt.hashSync('admin', bcrypt.genSaltSync()), 62 | token: randomString.generate(32), 63 | comment: 'Admin', 64 | createTime: (new Date()).getTime(), 65 | lastSeen: (new Date()).getTime(), 66 | from: 'Init', 67 | permission_group: 1 68 | }) 69 | } 70 | 71 | return null 72 | })() 73 | -------------------------------------------------------------------------------- /src/module/permission.js: -------------------------------------------------------------------------------- 1 | const db = require('./database') 2 | const user = require('./user') 3 | 4 | class Permission { 5 | /** 6 | * Get groups info by id 7 | * 8 | * @returns {Array} groups info 9 | */ 10 | async getPermissionGroupList (page, size) { 11 | const result = await db('permission_groups').select('*').paginate({ 12 | perPage: size, 13 | currentPage: page 14 | }) 15 | 16 | if (!result.data) { 17 | return { 18 | total: 0, 19 | data: [] 20 | } 21 | } 22 | 23 | let total = await db('permission_groups').count() 24 | total = total[0]['count(*)'] 25 | 26 | return { 27 | total, 28 | data: result.data 29 | } 30 | } 31 | 32 | /** 33 | * Create permission group 34 | * 35 | * @param {String} name group name 36 | * @param {Object} rule group rule 37 | * 38 | * @returns {Boolean} true 39 | */ 40 | async createPermissionGroup (name, rule) { 41 | await db('permission_groups').insert({ 42 | name, 43 | rule: JSON.stringify(rule), 44 | createTime: (new Date()).getTime(), 45 | updateTime: (new Date()).getTime() 46 | }) 47 | 48 | return true 49 | } 50 | 51 | /** 52 | * Change permisssion group 53 | * 54 | * @param {Int} gid group id 55 | * @param {String} name group name 56 | * @param {Object} rule group rule 57 | * 58 | * @returns {Int} 59 | */ 60 | async changePermissionGroup (id, name, rule) { 61 | const result = await db('permission_groups').where('id', id).update({ 62 | name, 63 | rule: JSON.stringify(rule), 64 | updateTime: (new Date()).getTime() 65 | }) 66 | 67 | return result 68 | } 69 | 70 | /** 71 | * Remove permission group 72 | * 73 | * @param {Int} id group id 74 | */ 75 | async removePermissionGroup (id) { 76 | const result = await db('permission_groups').where('id', id).delete() 77 | 78 | return result 79 | } 80 | 81 | /** 82 | * Get permission group info 83 | * 84 | * @param {Int} id group id 85 | * 86 | * @returns {Object} group info 87 | */ 88 | async getPermissionGroupInfo (id) { 89 | const result = await db('permission_groups').where('id', id).select('*').first() 90 | 91 | if (!result) return null 92 | 93 | return { 94 | ...result, 95 | rule: JSON.parse(result.rule) 96 | } 97 | } 98 | 99 | /** 100 | * Get user permission group info 101 | * 102 | * @param {Int} uid user id 103 | * 104 | * @returns {Object} group info 105 | */ 106 | async getUserPermissionGroupInfo (uid) { 107 | const userInfo = await user.getUserInfo(uid) 108 | const group = await this.getPermissionGroupInfo(userInfo.permission_group) 109 | 110 | return group 111 | } 112 | } 113 | 114 | module.exports = new Permission() 115 | -------------------------------------------------------------------------------- /src/api/route/announcement.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const announcement = require('./../../module/announcement') 4 | const permission = require('./../../module/permission') 5 | 6 | router.post('/createAnnouncement', async (req, res) => { 7 | const per = await permission.getUserPermissionGroupInfo(req.uid) 8 | if (!per.rule.admin) { 9 | res.status(403).json({ 10 | code: -1, 11 | msg: 'Access denied', 12 | data: {} 13 | }) 14 | return 15 | } 16 | 17 | const body = req.body 18 | if (!body || !body.title || !body.content) { 19 | res.json({ 20 | code: -2, 21 | msg: 'Param error', 22 | data: {} 23 | }) 24 | return 25 | } 26 | 27 | const result = await announcement.createAnnouncement(body.title, body.content) 28 | 29 | res.json({ 30 | code: 0, 31 | msg: 'Success', 32 | data: { 33 | code: result 34 | } 35 | }) 36 | }) 37 | 38 | router.post('/changeAnnouncement', async (req, res) => { 39 | const per = await permission.getUserPermissionGroupInfo(req.uid) 40 | if (!per.rule.admin) { 41 | res.status(403).json({ 42 | code: -1, 43 | msg: 'Access denied', 44 | data: {} 45 | }) 46 | return 47 | } 48 | 49 | const body = req.body 50 | if (!body || !body.id || !body.title || !body.content) { 51 | res.json({ 52 | code: -2, 53 | msg: 'Param error', 54 | data: {} 55 | }) 56 | return 57 | } 58 | 59 | const result = await announcement.changeAnnouncement(body.id, body.title, body.content) 60 | 61 | res.json({ 62 | code: 0, 63 | msg: 'Success', 64 | data: { 65 | code: result 66 | } 67 | }) 68 | }) 69 | 70 | router.post('/removeAnnouncement', async (req, res) => { 71 | const per = await permission.getUserPermissionGroupInfo(req.uid) 72 | if (!per.rule.admin) { 73 | res.status(403).json({ 74 | code: -1, 75 | msg: 'Access denied', 76 | data: {} 77 | }) 78 | return 79 | } 80 | 81 | const body = req.body 82 | if (!body || !body.id) { 83 | res.json({ 84 | code: -2, 85 | msg: 'Param error', 86 | data: {} 87 | }) 88 | return 89 | } 90 | 91 | const result = await announcement.removeAnnouncement(body.id) 92 | 93 | res.json({ 94 | code: 0, 95 | msg: 'Success', 96 | data: { 97 | code: result 98 | } 99 | }) 100 | }) 101 | 102 | router.get('/getAnnouncementList/:page?/:size?', async (req, res) => { 103 | let { page, size } = req.params 104 | page = parseInt(page || 1) 105 | size = parseInt(size || 20) 106 | 107 | if (page < 1 || size < 1 || size > 50) { 108 | res.json({ 109 | code: -2, 110 | msg: 'Param error', 111 | data: {} 112 | }) 113 | return 114 | } 115 | 116 | const result = await announcement.getAnnouncementList(page, size) 117 | 118 | res.json({ 119 | code: 0, 120 | msg: 'Success', 121 | data: { 122 | ...result 123 | } 124 | }) 125 | }) 126 | 127 | module.exports = router 128 | -------------------------------------------------------------------------------- /src/api/init.js: -------------------------------------------------------------------------------- 1 | const logger = require('./../module/logger')('API: Main') 2 | const express = require('express') 3 | const app = express() 4 | const bodyParser = require('body-parser') 5 | const cookieParser = require('cookie-parser') 6 | const user = require('./../module/user') 7 | const config = require('./../module/config') 8 | const cache = require('./../module/cache') 9 | const permission = require('./../module/permission') 10 | const Sentry = require('@sentry/node') 11 | 12 | const pathPrefix = config.get('system.path') 13 | 14 | Sentry.init({ 15 | dsn: 'https://a5df6f6888404ec492be93b7e93b5dd3@o230009.ingest.sentry.io/5217379' 16 | }) 17 | 18 | app.use(Sentry.Handlers.requestHandler()) 19 | app.use(Sentry.Handlers.errorHandler()) 20 | 21 | app.use(cookieParser()) 22 | app.use(bodyParser.json()) 23 | 24 | app.use((req, res, next) => { 25 | const whitelist = config.get('system.corsDomain') || [] 26 | const origin = req.headers.origin || '' 27 | 28 | if (whitelist.indexOf(origin) > -1) { 29 | res.setHeader('Access-Control-Allow-Origin', origin) 30 | res.setHeader('Access-Control-Allow-Methods', '*') 31 | res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept') 32 | res.setHeader('Access-Control-Allow-Credentials', 'true') 33 | } 34 | 35 | next() 36 | }) 37 | 38 | app.use(async (req, res, next) => { 39 | req.uid = -1 40 | 41 | const path = '' + req.path 42 | if (req.cookies && req.cookies.token) { 43 | const token = req.cookies.token 44 | const uid = await cache(`api_checktoken_${token}`, async () => { 45 | const res = await user.verifyToken(token) 46 | return res 47 | }, 60000) 48 | if (uid > 0) { 49 | const per = await cache(`api_checkpermission_${token}`, async () => { 50 | const res = await permission.getUserPermissionGroupInfo(uid) 51 | return res 52 | }, 60000) 53 | if (per.rule.banned) req.uid = -1 54 | else req.uid = uid 55 | } 56 | } 57 | 58 | logger.info(`[UID: ${req.uid}]`, req.method.toUpperCase(), req.path) 59 | 60 | if (path.startsWith(pathPrefix + '/auth') || req.uid > 0) return next() 61 | 62 | res.status(403).json({ 63 | code: -1, 64 | msg: 'Access denied', 65 | data: {} 66 | }) 67 | }) 68 | 69 | app.use(pathPrefix + '/auth', require('./route/auth')) 70 | app.use(pathPrefix + '/video', require('./route/video')) 71 | app.use(pathPrefix + '/metadata', require('./route/metadata')) 72 | app.use(pathPrefix + '/bookmark', require('./route/bookmark')) 73 | app.use(pathPrefix + '/file', require('./route/file')) 74 | app.use(pathPrefix + '/statistic', require('./route/statistic')) 75 | app.use(pathPrefix + '/user', require('./route/user')) 76 | app.use(pathPrefix + '/invitation', require('./route/invitation')) 77 | app.use(pathPrefix + '/group', require('./route/group')) 78 | app.use(pathPrefix + '/announcement', require('./route/announcement')) 79 | 80 | app.all('*', (req, res) => { 81 | res.status(404).json({ 82 | code: -2, 83 | msg: 'Not found', 84 | data: {} 85 | }) 86 | }) 87 | 88 | app.listen(config.get('system.port'), () => { 89 | logger.info(`JAVClub core is listening on port ${config.get('system.port')}!`) 90 | }) 91 | -------------------------------------------------------------------------------- /src/api/route/group.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const permission = require('./../../module/permission') 4 | 5 | router.get('/getGroupList/:page?/:size?', async (req, res) => { 6 | const per = await permission.getUserPermissionGroupInfo(req.uid) 7 | if (!per.rule.admin) { 8 | res.status(403).json({ 9 | code: -1, 10 | msg: 'Access denied', 11 | data: {} 12 | }) 13 | return 14 | } 15 | 16 | let { page, size } = req.params 17 | page = parseInt(page || 1) 18 | size = parseInt(size || 20) 19 | 20 | if (page < 1 || size < 1 || size > 50) { 21 | res.json({ 22 | code: -2, 23 | msg: 'Param error', 24 | data: {} 25 | }) 26 | return 27 | } 28 | 29 | const result = await permission.getPermissionGroupList(page, size) 30 | 31 | res.json({ 32 | code: 0, 33 | msg: 'Success', 34 | data: result 35 | }) 36 | }) 37 | 38 | router.post('/createGroup', async (req, res) => { 39 | const per = await permission.getUserPermissionGroupInfo(req.uid) 40 | if (!per.rule.admin) { 41 | res.status(403).json({ 42 | code: -1, 43 | msg: 'Access denied', 44 | data: {} 45 | }) 46 | return 47 | } 48 | 49 | const body = req.body 50 | if (!body || !body.name || !body.rule) { 51 | res.json({ 52 | code: -2, 53 | msg: 'Param error', 54 | data: {} 55 | }) 56 | return 57 | } 58 | 59 | const result = await permission.createPermissionGroup(body.name, JSON.parse(body.rule)) 60 | 61 | res.json({ 62 | code: 0, 63 | msg: 'Success', 64 | data: { 65 | result 66 | } 67 | }) 68 | }) 69 | 70 | router.post('/removeGroup', async (req, res) => { 71 | const per = await permission.getUserPermissionGroupInfo(req.uid) 72 | if (!per.rule.admin) { 73 | res.status(403).json({ 74 | code: -1, 75 | msg: 'Access denied', 76 | data: {} 77 | }) 78 | return 79 | } 80 | 81 | const body = req.body 82 | if (!body || !body.id) { 83 | res.json({ 84 | code: -2, 85 | msg: 'Param error', 86 | data: {} 87 | }) 88 | return 89 | } 90 | 91 | const result = await permission.removePermissionGroup(body.id) 92 | 93 | res.json({ 94 | code: 0, 95 | msg: 'Success', 96 | data: { 97 | result 98 | } 99 | }) 100 | }) 101 | 102 | router.post('/changeGroup', async (req, res) => { 103 | const per = await permission.getUserPermissionGroupInfo(req.uid) 104 | if (!per.rule.admin) { 105 | res.status(403).json({ 106 | code: -1, 107 | msg: 'Access denied', 108 | data: {} 109 | }) 110 | return 111 | } 112 | 113 | const body = req.body 114 | if (!body || !body.id || !body.name || !body.rule) { 115 | res.json({ 116 | code: -2, 117 | msg: 'Param error', 118 | data: {} 119 | }) 120 | return 121 | } 122 | 123 | const result = await permission.changePermissionGroup(body.id, body.name, JSON.parse(body.rule)) 124 | 125 | res.json({ 126 | code: 0, 127 | msg: 'Success', 128 | data: { 129 | result 130 | } 131 | }) 132 | }) 133 | 134 | module.exports = router 135 | -------------------------------------------------------------------------------- /src/module/file.js: -------------------------------------------------------------------------------- 1 | const db = require('./database') 2 | const logger = require('./logger')('Module: File') 3 | const stack = require('./stack') 4 | const config = require('./config') 5 | const randomInt = require('random-int') 6 | 7 | class File { 8 | /** 9 | * Create or get a record of file 10 | * 11 | * @param {Int} driverId 12 | * @param {Array} storageDataList 13 | * 14 | * @returns {Object} File ids 15 | */ 16 | async createFilesRecord (driverId, storageDataList) { 17 | // TODO: optimize 18 | logger.debug('Creating file record', storageDataList) 19 | 20 | const fileIds = {} 21 | 22 | let result = db('files').where('driverId', driverId).andWhere((builder) => { 23 | builder.where('storageData', storageDataList[0]) 24 | 25 | for (const i in storageDataList) { 26 | const item = storageDataList[i] 27 | if (i === 0) continue 28 | builder.orWhere('storageData', item) 29 | } 30 | }) 31 | 32 | result = await result.select('id', 'storageData') 33 | 34 | for (const i in result) { 35 | fileIds[result[i].storageData] = result[i].id 36 | } 37 | 38 | const oriKeys = storageDataList 39 | const nowKeys = Object.keys(fileIds) 40 | 41 | storageDataList = oriKeys.filter(o => { 42 | return nowKeys.indexOf(o) === -1 43 | }) 44 | 45 | const insertData = [] 46 | for (const i in storageDataList) { 47 | insertData.push({ 48 | driverId: driverId, 49 | storageData: storageDataList[i], 50 | updateTime: (new Date()).getTime() 51 | }) 52 | } 53 | 54 | result = await db('files').insert(insertData) 55 | 56 | if (result) { 57 | result = db('files').where('driverId', driverId).where('storageData', storageDataList[0]) 58 | for (const i in storageDataList) { 59 | const item = storageDataList[i] 60 | if (i === 0) continue 61 | result = result.orWhere('storageData', item) 62 | } 63 | result = await result.select('id', 'storageData') 64 | 65 | for (const i in result) { 66 | fileIds[result[i].storageData] = result[i].id 67 | } 68 | } 69 | 70 | return fileIds 71 | } 72 | 73 | /** 74 | * 75 | * @param {Array} fileIds 76 | */ 77 | async getFilesURL (fileIds) { 78 | let result = db('files').where('id', fileIds[0]) 79 | for (const id in fileIds) { 80 | if (id === 0) continue 81 | const fileId = fileIds[id] 82 | result = result.orWhere('id', fileId) 83 | } 84 | result = await result.select('*') 85 | 86 | if (!result) return '' 87 | logger.debug('Files record:', result) 88 | const url = {} 89 | 90 | for (const i in result) { 91 | const item = result[i] 92 | const client = await stack.getInstance(item.driverId) 93 | const res = await client.getFileURL(JSON.parse(item.storageData)) 94 | url[item.id] = res 95 | } 96 | 97 | return url 98 | } 99 | 100 | getProxyPrefix () { 101 | const proxyList = config.get('proxy') 102 | if (!proxyList || proxyList.length < 1) return '' 103 | 104 | return proxyList[randomInt(0, proxyList.length - 1)] 105 | } 106 | } 107 | 108 | module.exports = new File() 109 | -------------------------------------------------------------------------------- /src/api/route/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const user = require('./../../module/user') 4 | const invitation = require('./../../module/invitation') 5 | const permission = require('./../../module/permission') 6 | const config = require('../../module/config') 7 | 8 | router.post('/login', async (req, res) => { 9 | const body = req.body 10 | if (body && body.username && body.password) { 11 | const result = await user.checkByUsernameAndPassword(body.username, body.password) 12 | 13 | if (result.token) { 14 | const per = await permission.getUserPermissionGroupInfo(result.id) 15 | if (per.rule.banned) { 16 | res.json({ 17 | code: -1, 18 | msg: 'You had been banned', 19 | data: {} 20 | }) 21 | return 22 | } 23 | res.cookie('token', result.token, { 24 | maxAge: (new Date()).getTime() / 1000 + 1000 * 3600 * 24 * 180, 25 | path: '/' 26 | }) 27 | 28 | res.json({ 29 | code: 0, 30 | msg: 'Success', 31 | data: { 32 | token: result.token 33 | } 34 | }) 35 | return 36 | } else { 37 | res.json({ 38 | code: -1, 39 | msg: 'Username or password wrong', 40 | data: {} 41 | }) 42 | return 43 | } 44 | } 45 | 46 | res.json({ 47 | code: -2, 48 | msg: 'Invalid body', 49 | data: {} 50 | }) 51 | }) 52 | 53 | router.post('/signup', async (req, res) => { 54 | const body = req.body 55 | if (body && body.username && body.password) { 56 | if (!config.get('system.allowSignup') && !body.code) { 57 | res.json({ 58 | code: -2, 59 | msg: 'Param error', 60 | data: {} 61 | }) 62 | 63 | return 64 | } 65 | 66 | const username = `${body.username}`.substring(0, 32) 67 | 68 | if (config.get('system.allowSignup')) { 69 | const uid = await user.createUser(username, body.password, config.get('system.defaultGroup'), '', 'direct signup') 70 | if (uid === -1) { 71 | res.json({ 72 | code: -2, 73 | msg: 'Username exists', 74 | data: {} 75 | }) 76 | return 77 | } 78 | 79 | res.json({ 80 | code: 0, 81 | msg: 'Success', 82 | data: { 83 | uid 84 | } 85 | }) 86 | return 87 | } 88 | 89 | res.json(await invitation.createUserUseInvitation(body.code, username, body.password)) 90 | return 91 | } 92 | 93 | res.json({ 94 | code: -2, 95 | msg: 'Invalid body', 96 | data: {} 97 | }) 98 | }) 99 | 100 | router.get('/check', async (req, res) => { 101 | let result = false 102 | if (req.uid && req.uid > 0) result = true 103 | 104 | if (result) { 105 | const group = await permission.getUserPermissionGroupInfo(req.uid) 106 | 107 | res.json({ 108 | code: 0, 109 | msg: 'Success', 110 | data: { 111 | isLogin: true, 112 | permission: group 113 | } 114 | }) 115 | } else { 116 | res.json({ 117 | code: 0, 118 | msg: 'Success', 119 | data: { 120 | isLogin: false 121 | } 122 | }) 123 | } 124 | }) 125 | 126 | router.get('/getStatus', async (req, res) => { 127 | res.json({ 128 | code: 0, 129 | msg: 'Success', 130 | data: { 131 | allowSignup: config.get('system.allowSignup') 132 | } 133 | }) 134 | }) 135 | 136 | router.all('/logout', (req, res) => { 137 | res.clearCookie('token') 138 | res.json({ 139 | code: 0, 140 | msg: 'Success', 141 | data: {} 142 | }) 143 | }) 144 | 145 | module.exports = router 146 | -------------------------------------------------------------------------------- /src/api/route/metadata.js: -------------------------------------------------------------------------------- 1 | const logger = require('./../../module/logger')('API: Metadata') 2 | const express = require('express') 3 | const router = express.Router() 4 | const cache = require('./../../module/cache') 5 | const metadata = require('./../../module/metadata') 6 | 7 | router.get('/getInfo/:metadataId', async (req, res) => { 8 | let { metadataId } = req.params 9 | metadataId = parseInt(metadataId || 1) 10 | logger.debug(`Metadata id ${metadataId}`) 11 | 12 | if (metadataId < 1) { 13 | res.json({ 14 | code: -2, 15 | msg: 'Param error', 16 | data: {} 17 | }) 18 | return 19 | } 20 | 21 | const result = await cache(`api_metadata_info_${metadataId}`, async () => { 22 | const res = await metadata.getMetadataById(metadataId) 23 | return res 24 | }, 60000) 25 | 26 | res.json({ 27 | code: 0, 28 | msg: 'Success', 29 | data: result 30 | }) 31 | }) 32 | 33 | router.get('/getList/:page?/:size?', async (req, res) => { 34 | let { page, size } = req.params 35 | page = parseInt(page || 1) 36 | size = parseInt(size || 20) 37 | logger.debug(`Page ${page}, size ${size}`) 38 | 39 | if (page < 1 || size < 1 || size > 50) { 40 | res.json({ 41 | code: -2, 42 | msg: 'Param error', 43 | data: {} 44 | }) 45 | return 46 | } 47 | 48 | const result = await cache(`api_metadata_list_${page}_${size}`, async () => { 49 | const res = await metadata.getMetadataList(page, size) 50 | return res 51 | }, 60000) 52 | 53 | res.json({ 54 | code: 0, 55 | msg: 'Success', 56 | data: result 57 | }) 58 | }) 59 | 60 | router.get('/getListByMeta/:type/:metaId/:page?/:size?', async (req, res) => { 61 | let { type, metaId, page, size } = req.params 62 | page = parseInt(page || 1) 63 | size = parseInt(size || 20) 64 | logger.debug(`Type ${type}, metaId ${metaId}, page ${page}, size ${size}`) 65 | 66 | if (page < 1 || size < 1 || size > 50) { 67 | res.json({ 68 | code: -2, 69 | msg: 'Param error', 70 | data: {} 71 | }) 72 | return 73 | } 74 | 75 | const result = await cache(`api_metadata_listbymeta_${metaId}_${page}_${size}`, async () => { 76 | const res = await metadata.getMetadataListByMetaId(type, metaId, page, size) 77 | return res 78 | }, 60000) 79 | 80 | res.json({ 81 | code: 0, 82 | msg: 'Success', 83 | data: result 84 | }) 85 | }) 86 | 87 | router.get('/getMetaList/:type/:page?/:size?', async (req, res) => { 88 | let { type, page, size } = req.params 89 | page = parseInt(page || 1) 90 | size = parseInt(size || 20) 91 | logger.debug(`Type ${type}, page ${page}, size ${size}`) 92 | 93 | if (page < 1 || size < 1 || size > 50) { 94 | res.json({ 95 | code: -2, 96 | msg: 'Param error', 97 | data: {} 98 | }) 99 | return 100 | } 101 | 102 | type = metadata._getTypeMapping(type).type 103 | 104 | const result = await cache(`api_meta_list_${type}_${page}_${size}`, async () => { 105 | const res = await metadata.getMetaList(type, page, size) 106 | return res 107 | }, 60000) 108 | 109 | res.json({ 110 | code: 0, 111 | msg: 'Success', 112 | data: result 113 | }) 114 | }) 115 | 116 | router.get('/search/:str/:page?/:size?', async (req, res) => { 117 | let { str, page, size } = req.params 118 | str = `${str}` 119 | page = parseInt(page || 1) 120 | size = parseInt(size || 20) 121 | logger.debug(`Search string ${str}, page ${page}, size ${size}`) 122 | 123 | if (page < 1 || size < 1 || size > 50 || str.length <= 0) { 124 | res.json({ 125 | code: -2, 126 | msg: 'Param error', 127 | data: {} 128 | }) 129 | return 130 | } 131 | 132 | const result = await cache(`api_metadata_search_${str}_${page}_${size}`, async () => { 133 | const res = await metadata.searchMetadata(str, page, size) 134 | return res 135 | }, 60000) 136 | 137 | res.json({ 138 | code: 0, 139 | msg: 'Success', 140 | data: result 141 | }) 142 | }) 143 | 144 | module.exports = router 145 | -------------------------------------------------------------------------------- /src/module/invitation.js: -------------------------------------------------------------------------------- 1 | const logger = require('./logger')('Module: Invitation') 2 | const db = require('./database') 3 | const randomString = require('randomstring') 4 | const permission = require('./permission') 5 | const user = require('./user') 6 | 7 | class Invitation { 8 | /** 9 | * Create invitation code 10 | * 11 | * @param {Int} uid user id 12 | * 13 | * @returns {String} invitation code 14 | */ 15 | async createInvitation (uid) { 16 | const code = randomString.generate(24) 17 | const groupInfo = await permission.getUserPermissionGroupInfo(uid) 18 | 19 | await db('invitations').insert({ 20 | creator: uid, 21 | code, 22 | permission_group: groupInfo.rule.invitationGroup, 23 | createTime: (new Date()).getTime() 24 | }) 25 | 26 | return code 27 | } 28 | 29 | /** 30 | * Use invitation code 31 | * 32 | * @param {Int} uid user id 33 | * @param {String} code invitation code 34 | * 35 | * @returns {Int} 36 | */ 37 | async useInvitation (uid, code) { 38 | const result = await db('invitations').where('code', code).update({ 39 | useBy: uid, 40 | useTime: (new Date()).getTime() 41 | }) 42 | 43 | return result 44 | } 45 | 46 | /** 47 | * Get invitation info 48 | * 49 | * @param {String} code invitation code 50 | * 51 | * @returns {Object} invitation info 52 | */ 53 | async getInvitationInfo (code) { 54 | const result = await db('invitations').where('code', code).select('*').first() 55 | 56 | return result 57 | } 58 | 59 | /** 60 | * Create user using invitation code 61 | * 62 | * @param {String} code invitation code 63 | * @param {String} username username 64 | * @param {String} password password 65 | */ 66 | async createUserUseInvitation (code, username, password) { 67 | const codeStatus = await this.verifyInvitation(code) 68 | if (!codeStatus) { 69 | return { 70 | code: -1, 71 | msg: 'Code invalid', 72 | data: {} 73 | } 74 | } 75 | 76 | const codeInfo = await this.getInvitationInfo(code) 77 | const group = codeInfo.permission_group 78 | const uid = await user.createUser(username, password, group, 'Using invitation', 'invitation code') 79 | if (uid === -1) { 80 | return { 81 | code: -2, 82 | msg: 'Username exists', 83 | data: {} 84 | } 85 | } 86 | await this.useInvitation(uid, code) 87 | 88 | return { 89 | code: 0, 90 | msg: 'Success', 91 | data: { 92 | uid 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Get user invitation list 99 | * 100 | * @param {Int=} uid user id 101 | * @param {Int=} page page number 102 | * @param {Int=} size page size 103 | * 104 | * @returns {Array} invitation list 105 | */ 106 | async getUserInvitation (uid = -1, page = 1, size = 20) { 107 | let result = db('invitations') 108 | if (uid !== -1) result = result.where('creator', uid) 109 | result = await result.orderBy('id', 'desc').select('*').paginate({ 110 | perPage: size, 111 | currentPage: page 112 | }) 113 | 114 | if (!result.data) { 115 | return { 116 | total: 0, 117 | data: [] 118 | } 119 | } 120 | 121 | let total = db('invitations') 122 | if (uid !== -1) total = total.where('creator', uid) 123 | total = (await total.count())[0]['count(*)'] 124 | 125 | return { 126 | total, 127 | data: result.data 128 | } 129 | } 130 | 131 | /** 132 | * Check user invitation limit 133 | * 134 | * @param {Int} uid user id 135 | * @param {Boolean} boolean whether return boolean 136 | * 137 | * @returns {Boolean|Object} 138 | */ 139 | async checkUserInvitationLimit (uid, boolean = true) { 140 | const currentYear = (new Date()).getFullYear() 141 | const currentMonth = (new Date()).getMonth() + 1 142 | 143 | const monthStart = (new Date(`${currentYear}-${currentMonth}-1`)).getTime() 144 | const monthEnd = (new Date(`${currentYear}-${currentMonth + 1}-1`)).getTime() 145 | 146 | const codeNum = (await db('invitations').where('creator', uid).whereBetween('createTime', [monthStart, monthEnd]).count('*'))[0]['count(*)'] 147 | const permissionInfo = await permission.getUserPermissionGroupInfo(uid) 148 | const invitationNum = permissionInfo.rule.invitationNum 149 | 150 | if (boolean) { 151 | if (codeNum >= invitationNum && invitationNum !== -1) return false 152 | return true 153 | } else { 154 | return { 155 | codeNum, 156 | invitationNum 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * Verify invitation code 163 | * 164 | * @param {String} code invitation code 165 | * 166 | * @returns {Boolean} invitation code status 167 | */ 168 | async verifyInvitation (code) { 169 | logger.debug('Checking invitation code', code) 170 | const result = await db('invitations').where('code', code).select('useBy').first() 171 | 172 | if (result) { 173 | if (result.useBy === null) { 174 | return true 175 | } 176 | } 177 | 178 | return false 179 | } 180 | } 181 | 182 | module.exports = new Invitation() 183 | -------------------------------------------------------------------------------- /src/api/route/bookmark.js: -------------------------------------------------------------------------------- 1 | const logger = require('./../../module/logger')('API: Bookmark') 2 | const express = require('express') 3 | const router = express.Router() 4 | const config = require('./../../module/config') 5 | const bookmark = require('./../../module/bookmark') 6 | 7 | router.get('/getList', async (req, res) => { 8 | const result = await bookmark.getUserBookmarkList(req.uid) 9 | 10 | res.json({ 11 | code: 0, 12 | msg: 'Success', 13 | data: result 14 | }) 15 | }) 16 | 17 | router.get('/getInfo/:bookmarkId/:page?/:size?', async (req, res) => { 18 | let { bookmarkId, page, size } = req.params 19 | bookmarkId = parseInt(bookmarkId) 20 | page = parseInt(page || 1) 21 | size = parseInt(size || 20) 22 | logger.debug(`Page ${page}, size ${size}`) 23 | 24 | if (page < 1 || size < 1 || size > 50) { 25 | res.json({ 26 | code: -2, 27 | msg: 'Param error', 28 | data: {} 29 | }) 30 | return 31 | } 32 | 33 | if (!await bookmark.isOwn(req.uid, bookmarkId)) { 34 | res.status(403).json({ 35 | code: -1, 36 | msg: 'Access denied', 37 | data: {} 38 | }) 39 | return 40 | } 41 | 42 | const result = await bookmark.getBookmarkInfo(bookmarkId, false, page, size) 43 | 44 | res.json({ 45 | code: 0, 46 | msg: 'Success', 47 | data: result 48 | }) 49 | }) 50 | 51 | router.post('/addMetadata/:bookmarkId', async (req, res) => { 52 | const bookmarkId = parseInt(req.params.bookmarkId || 0) 53 | 54 | if (!await bookmark.isOwn(req.uid, bookmarkId)) { 55 | res.status(403).json({ 56 | code: -1, 57 | msg: 'Access denied', 58 | data: {} 59 | }) 60 | return 61 | } 62 | 63 | const body = req.body 64 | if (!body || !body.metadataId) { 65 | res.json({ 66 | code: -2, 67 | msg: 'Param error', 68 | data: {} 69 | }) 70 | return 71 | } 72 | 73 | const maxNum = config.get('system.userMaxBookmarkItemNum') || 100 74 | if (await bookmark.getBookmarkInfoNum(bookmarkId) >= maxNum) { 75 | res.json({ 76 | code: -2, 77 | msg: `You have reached a limit of ${maxNum} items in a single bookmark`, 78 | data: {} 79 | }) 80 | return 81 | } 82 | 83 | const result = await bookmark.addMetadata(bookmarkId, body.metadataId) 84 | 85 | res.json({ 86 | code: 0, 87 | msg: 'Success', 88 | data: { 89 | result: result 90 | } 91 | }) 92 | }) 93 | 94 | router.post('/createBookmark', async (req, res) => { 95 | const body = req.body 96 | if (!body || !body.name) { 97 | res.json({ 98 | code: -2, 99 | msg: 'Param error', 100 | data: {} 101 | }) 102 | return 103 | } 104 | const bookmarkName = `${body.name}`.substring(0, 64) 105 | 106 | const maxNum = config.get('system.userMaxBookmarkNum') || 10 107 | if (await bookmark.getBookmarkNumByUserId(req.uid) >= maxNum) { 108 | res.json({ 109 | code: -2, 110 | msg: `You have reached a limit of ${maxNum} bookmarks for a single user`, 111 | data: {} 112 | }) 113 | return 114 | } 115 | 116 | const result = await bookmark.createBookmark(req.uid, bookmarkName) 117 | 118 | res.json({ 119 | code: 0, 120 | msg: 'Success', 121 | data: { 122 | result: result 123 | } 124 | }) 125 | }) 126 | 127 | router.post('/removeBookmark', async (req, res) => { 128 | const body = req.body 129 | if (!body || !body.bookmarkId) { 130 | res.json({ 131 | code: -2, 132 | msg: 'Param error', 133 | data: {} 134 | }) 135 | return 136 | } 137 | const bookmarkId = parseInt(body.bookmarkId) || 0 138 | 139 | if (!await bookmark.isOwn(req.uid, bookmarkId)) { 140 | res.status(403).json({ 141 | code: -1, 142 | msg: 'Access denied', 143 | data: {} 144 | }) 145 | return 146 | } 147 | 148 | const result = await bookmark.removeBookmark(bookmarkId) 149 | 150 | res.json({ 151 | code: 0, 152 | msg: 'Success', 153 | data: { 154 | result: result 155 | } 156 | }) 157 | }) 158 | 159 | router.post('/removeMetadata/:bookmarkId', async (req, res) => { 160 | const bookmarkId = parseInt(req.params.bookmarkId || 0) 161 | 162 | if (!await bookmark.isOwn(req.uid, bookmarkId)) { 163 | res.status(403).json({ 164 | code: -1, 165 | msg: 'Access denied', 166 | data: {} 167 | }) 168 | return 169 | } 170 | 171 | const body = req.body 172 | if (!body || !body.metadataId) { 173 | res.json({ 174 | code: -2, 175 | msg: 'Param error', 176 | data: {} 177 | }) 178 | return 179 | } 180 | 181 | const result = await bookmark.removeMetadata(bookmarkId, body.metadataId) 182 | 183 | res.json({ 184 | code: 0, 185 | msg: 'Success', 186 | data: { 187 | result: result 188 | } 189 | }) 190 | }) 191 | 192 | router.get('/getByMetadata/:metadataId', async (req, res) => { 193 | const metadataId = parseInt(req.params.metadataId) 194 | 195 | if (metadataId < 1) { 196 | res.json({ 197 | code: -2, 198 | msg: 'Param error', 199 | data: {} 200 | }) 201 | return 202 | } 203 | 204 | const result = await bookmark.getBookmarkByMetadataId(req.uid, metadataId) 205 | 206 | res.json({ 207 | code: 0, 208 | msg: 'Success', 209 | data: result 210 | }) 211 | }) 212 | 213 | module.exports = router 214 | -------------------------------------------------------------------------------- /src/module/video.js: -------------------------------------------------------------------------------- 1 | const db = require('./database') 2 | const logger = require('./logger')('Module: Video') 3 | const metadata = require('./metadata') 4 | 5 | class Video { 6 | /** 7 | * Get video info by id 8 | * 9 | * @param {Int} id video id 10 | * 11 | * @returns {Object} video info 12 | */ 13 | async getVideoInfo (id) { 14 | logger.debug('Get video info, id', id) 15 | const result = await db('videos').where('id', id).select('*').first() 16 | 17 | if (!result) return null 18 | 19 | return { 20 | id: result.id, 21 | metadataId: result.metadataId, 22 | videoFileId: result.videoFileId, 23 | isHiden: (result.isHiden === 1), 24 | infoFileId: result.infoFileId, 25 | videoMetadata: JSON.parse(result.videoMetadata), 26 | storyboardFileIdSet: JSON.parse(result.storyboardFileIdSet), 27 | version: parseInt(result.version) || 1, 28 | updateTime: result.updateTime 29 | } 30 | } 31 | 32 | /** 33 | * Get video list 34 | * 35 | * @param {Int=} page page number 36 | * @param {Int=} size page size 37 | * @param {Boolean=} showHiden show hiden video 38 | * @param {Int=} get list by metadata id 39 | * 40 | * @returns {Array} video info list 41 | */ 42 | async getVideoList (page = 1, size = 20, showHiden = false, metadataId = 0) { 43 | let result 44 | result = db('videos') 45 | if (!showHiden) result = result.where('isHiden', 0) 46 | if (metadataId !== 0) result = result.where('metadataId', metadataId) 47 | result = await result.orderBy('id', 'desc').select('*').paginate({ 48 | perPage: size, 49 | currentPage: page 50 | }) 51 | 52 | result = result.data 53 | if (!result) return [] 54 | 55 | const processed = [] 56 | for (const i in result) { 57 | const item = result[i] 58 | processed.push({ 59 | id: item.id, 60 | metadataId: item.metadataId, 61 | videoFileId: item.videoFileId, 62 | isHiden: (item.isHiden === 1), 63 | infoFileId: item.infoFileId, 64 | videoMetadata: JSON.parse(item.videoMetadata), 65 | storyboardFileIdSet: JSON.parse(item.storyboardFileIdSet || '[]'), 66 | version: parseInt(item.version) || 1, 67 | updateTime: item.updateTime 68 | }) 69 | } 70 | 71 | return processed 72 | } 73 | 74 | /** 75 | * Hide video by video id 76 | * 77 | * @param {Int} id video id 78 | * 79 | * @returns {Boolean} 80 | */ 81 | async hideVideo (id) { 82 | if (await db('videos').where('id', id).update('isHiden', 1)) return true 83 | return false 84 | } 85 | 86 | /** 87 | * Unhide video by video id 88 | * 89 | * @param {Int} id video id 90 | * 91 | * @returns {Boolean} 92 | */ 93 | async unhideVideo (id) { 94 | if (await db('videos').where('id', id).update('isHiden', 0)) return true 95 | return false 96 | } 97 | 98 | /** 99 | * Create video record 100 | * 101 | * @param {Object} info JAV info 102 | * @param {Object} fileIds File ids 103 | * @param {Int} fileIds.metaId info.js file id 104 | * @param {Int} fileIds.videoId video.mp4 file id 105 | * @param {Int} fileIds[storyboardId].id storyboard file id 106 | * 107 | * @returns {Int} Video id 108 | */ 109 | async createVideo (info, fileIds, version = 1) { 110 | if (info.company && info.id) info.JAVID = info.company + '-' + info.id 111 | 112 | const metadataId = await metadata.getMetadataId(info.JAVID, version, info.JAVMetadata) 113 | logger.debug('Metadata id', metadataId) 114 | 115 | if (!metadataId || metadata === 0) { 116 | return 117 | } 118 | 119 | if (info.JAVMetadata) { 120 | delete info.JAVMetadata 121 | info.metadata = info.videoMetadata 122 | delete info.videoMetadata 123 | } 124 | 125 | const dbData = { 126 | videoMetadata: JSON.stringify(info), 127 | isHiden: 0, 128 | videoFileId: fileIds.videoId, 129 | metadataId: metadataId, 130 | infoFileId: fileIds.metaId, 131 | updateTime: (new Date()).getTime(), 132 | version 133 | } 134 | 135 | if (version === 1) dbData.storyboardFileIdSet = JSON.stringify(fileIds.storyboardId) 136 | else dbData.storyboardFileIdSet = '[]' 137 | 138 | const result = await db('videos').insert(dbData).select('id') 139 | 140 | logger.info(`[${info.JAVID}] Video created, id`, result[0]) 141 | return result[0] 142 | } 143 | 144 | /** 145 | * Check video status by meta hash 146 | * 147 | * @param {String} hash video meta hash 148 | * 149 | * @returns {Boolean} 150 | */ 151 | async isExistByHash (hash) { 152 | const result = await db('videos').where('videoMetadata', 'like', `%${hash}%`).count() 153 | if (result && result[0]['count(*)'] === 0) return false 154 | return true 155 | } 156 | 157 | /** 158 | * Get video id by info.json file id 159 | * 160 | * @param {String} infoFileId 161 | * 162 | * @returns {Int} video id 163 | */ 164 | async getVideoIdByInfoFileId (infoFileId) { 165 | const result = await db('videos').where('infoFileId', infoFileId).select('id').first() 166 | 167 | if (result && result.id) { 168 | return result.id 169 | } else return 0 170 | } 171 | } 172 | 173 | module.exports = new Video() 174 | -------------------------------------------------------------------------------- /src/module/user.js: -------------------------------------------------------------------------------- 1 | const logger = require('./logger')('Module: User') 2 | const db = require('./database') 3 | const bcrypt = require('bcrypt') 4 | const randomString = require('randomstring') 5 | 6 | class User { 7 | /** 8 | * Get users info by id 9 | * 10 | * @returns {Array} users info 11 | */ 12 | async getUserList (page, size) { 13 | const result = await db('users').select('*').paginate({ 14 | perPage: size, 15 | currentPage: page 16 | }) 17 | 18 | if (!result.data) { 19 | return { 20 | total: 0, 21 | data: [] 22 | } 23 | } 24 | 25 | let total = await db('users').count() 26 | total = total[0]['count(*)'] 27 | 28 | return { 29 | total, 30 | data: result.data 31 | } 32 | } 33 | 34 | /** 35 | * Create user 36 | * 37 | * @param {String} username username 38 | * @param {String} password user password 39 | * @param {Int} groupId permission group id 40 | * @param {String=} comment comment 41 | * @param {String=} from from 42 | * 43 | * @returns {Int} username id 44 | */ 45 | async createUser (username, password, groupId, comment = '', from = '') { 46 | if (!await this.checkUsername(username)) return -1 47 | 48 | const result = await db('users').insert({ 49 | username, 50 | password: bcrypt.hashSync(password, bcrypt.genSaltSync()), 51 | token: randomString.generate(32), 52 | permission_group: groupId, 53 | from, 54 | comment, 55 | createTime: (new Date()).getTime(), 56 | lastSeen: (new Date()).getTime() 57 | }).select('id') 58 | 59 | return result[0] 60 | } 61 | 62 | /** 63 | * Check username availability 64 | * 65 | * @param {String} username username 66 | */ 67 | async checkUsername (username) { 68 | const result = (await db('users').where('username', username).count('*'))[0]['count(*)'] 69 | 70 | return result === 0 71 | } 72 | 73 | /** 74 | * Remove user 75 | * 76 | * @param {Int} uid user id 77 | */ 78 | async removeUser (uid) { 79 | const result = await db('users').where('id', uid).delete() 80 | 81 | return result 82 | } 83 | 84 | /** 85 | * Check by username and password 86 | * 87 | * @param {String} username username 88 | * @param {String} password password 89 | * 90 | * @returns {Object} token and uid 91 | */ 92 | async checkByUsernameAndPassword (username, password) { 93 | const result = await db('users').where('username', username).select('*').first() 94 | 95 | if (result && result.password) { 96 | if (bcrypt.compareSync(password, result.password)) { 97 | await db('users').where('token', result.token).update('lastSeen', (new Date()).getTime()) 98 | 99 | return { 100 | token: result.token, 101 | id: result.id 102 | } 103 | } 104 | } 105 | 106 | return {} 107 | } 108 | 109 | /** 110 | * Get user info by id 111 | * 112 | * @param {Int} id user id 113 | * 114 | * @returns {Object} user info 115 | */ 116 | async getUserInfo (id) { 117 | const result = await db('users').where('id', id).select('*').first() 118 | 119 | if (!result) return null 120 | 121 | return result 122 | } 123 | 124 | /** 125 | * Change user's username 126 | * 127 | * @param {Int} uid user id 128 | * @param {String} newUsername new username 129 | * 130 | * @returns {Int} 131 | */ 132 | async changeUsername (uid, newUsername) { 133 | if (!await this.checkUsername(newUsername)) return -1 134 | const result = await db('users').where('id', uid).update({ 135 | username: newUsername, 136 | lastSeen: (new Date()).getTime() 137 | }) 138 | 139 | return result 140 | } 141 | 142 | /** 143 | * Change user's password 144 | * 145 | * @param {Int} uid user id 146 | * @param {String} newPassword new password 147 | * 148 | * @returns {Int} 149 | */ 150 | async changePassword (uid, newPassword) { 151 | const password = bcrypt.hashSync(newPassword, bcrypt.genSaltSync()) 152 | 153 | const result = await db('users').where('id', uid).update({ 154 | password: password, 155 | token: randomString.generate(32), 156 | lastSeen: (new Date()).getTime() 157 | }) 158 | 159 | return result 160 | } 161 | 162 | /** 163 | * Change user's group 164 | * 165 | * @param {Int} uid user id 166 | * @param {String} newGroupId new permission group id 167 | * 168 | * @returns {Int} 169 | */ 170 | async changeGroup (uid, newGroupId) { 171 | const result = await db('users').where('id', uid).update({ 172 | permission_group: newGroupId 173 | }) 174 | 175 | return result 176 | } 177 | 178 | /** 179 | * Change user's comment 180 | * 181 | * @param {Int} uid user id 182 | * @param {String} newComment new comment 183 | * 184 | * @returns {Int} 185 | */ 186 | async changeComment (uid, newComment) { 187 | const result = await db('users').where('id', uid).update({ 188 | comment: newComment 189 | }) 190 | 191 | return result 192 | } 193 | 194 | /** 195 | * Verify token 196 | * 197 | * @param {String} token token 198 | * 199 | * @returns {Int} user id 200 | */ 201 | async verifyToken (token) { 202 | logger.debug('Checking token', token) 203 | const result = await db('users').where('token', token).select('id').first() 204 | 205 | if (result && result.id > 0) { 206 | await db('users').where('token', token).update('lastSeen', (new Date()).getTime()) 207 | 208 | return result.id 209 | } 210 | 211 | return 0 212 | } 213 | } 214 | 215 | module.exports = new User() 216 | -------------------------------------------------------------------------------- /src/import/driver/googleDrive.js: -------------------------------------------------------------------------------- 1 | const config = require('./../../module/config') 2 | const file = require('./../../module/file') 3 | const video = require('./../../module/video') 4 | const ignore = require('./../../module/ignore') 5 | 6 | class googleDrive { 7 | /** 8 | * @param {Int} id driver id 9 | * @param {Object} driver Google Drive driver instance 10 | */ 11 | constructor (id, driver) { 12 | this.id = id 13 | this.client = driver 14 | this.logger = require('./../../module/logger')('Importer: GD ' + id) 15 | 16 | if (!this.client) throw new Error('Invaild drive instance') 17 | this.logger.info('Got drive instance') 18 | 19 | const { 20 | default: PQueue 21 | } = require('p-queue') 22 | 23 | this.queue = new PQueue({ 24 | concurrency: config.get('importer.settings.googleDrive.queueNum') || 1 25 | }) 26 | } 27 | 28 | /** 29 | * Entry for Google Drive importer 30 | * 31 | * @param {Boolean} full Scan the whole drive 32 | * 33 | * @returns {Promise} Promise queue 34 | */ 35 | async run (full = false) { 36 | this.logger.info('Starting process of import, full =', full) 37 | 38 | const fileList = await this.client.getFileList('name=\'info.json\'', null, full, 'modifiedTime desc', (full) ? null : 51) 39 | this.logger.debug('Got info.json file list') 40 | 41 | fileList.forEach((info) => { 42 | this.queue.add(async () => { 43 | this.logger.debug('Handling info.json file', info.id) 44 | 45 | let res = await this.client.downloadFile(info.id) 46 | res = res.toString() 47 | 48 | this.logger.debug(`File ${info.id}'s content`, res) 49 | try { 50 | if (res && JSON.parse(res)) { 51 | return await this.handleInfoDotJSON(JSON.parse(res), info) 52 | } else { 53 | this.logger.error('Invalid info.json content', info.id, res) 54 | } 55 | } catch (error) { 56 | this.logger.error('Info.json parsed error, skipped') 57 | } 58 | }) 59 | }) 60 | 61 | const result = await this.queue.onIdle().then(() => { 62 | this.logger.info('All Promise settled') 63 | }) 64 | 65 | return result 66 | } 67 | 68 | /** 69 | * Handle contents of info.json 70 | * 71 | * @param {String} info info.js file content 72 | * @param {String} fileInfo Google Drive file info 73 | * 74 | * @returns {Promise} Video create Promise 75 | */ 76 | async handleInfoDotJSON (info, fileInfo) { 77 | this.logger.debug('Info', info) 78 | if (!info.JAVID && (!info.company || !info.id)) { 79 | this.logger.warn('Info invalid', info) 80 | return 81 | } 82 | 83 | let JAVID = info.JAVID 84 | if (info.company && info.id) JAVID = info.company + '-' + info.id 85 | 86 | const version = parseInt(info.version || 1) 87 | this.logger.info('Processing', JAVID) 88 | 89 | this.logger.debug(`${JAVID} info.json file version:`, version) 90 | if (await ignore.checkIgnoreStatus(JAVID)) { 91 | this.logger.debug(`Metadata ${JAVID} invalid, skipped`) 92 | return 93 | } 94 | 95 | if (await video.isExistByHash(info.hash)) { 96 | this.logger.info(`Video ${info.hash} existed, skipped`) 97 | return 98 | } 99 | const parent = fileInfo.parents[0] 100 | 101 | this.logger.debug('Video folder id', parent) 102 | 103 | const fileList = await this.client.getFileList(`'${parent}' in parents`) 104 | this.logger.debug('Video folder file list', fileList) 105 | 106 | let storyboardId, videoId 107 | for (const i in fileList) { 108 | const item = fileList[i] 109 | if (item.name === 'video.mp4' && (item.size / 1024 / 1024) > 100) videoId = item.id 110 | if (item.name === 'storyboard') storyboardId = item.id 111 | } 112 | 113 | this.logger.debug('Video id', videoId) 114 | 115 | let storyboardList = [] 116 | if (version === 1) { 117 | this.logger.debug('Storyboard folder id', storyboardId) 118 | 119 | storyboardList = await this.client.getFileList(`'${storyboardId}' in parents`) 120 | } 121 | 122 | if ((version === 1 && storyboardList.length !== 50) || !videoId) { 123 | this.logger.info(`Video ${info.hash} havn't fully upload yet`) 124 | return 125 | } 126 | 127 | this.logger.debug(JAVID, 'check pass') 128 | const fileIds = await this.createFileRecord({ 129 | videoId, 130 | storyboardList, 131 | fileInfo 132 | }, version) 133 | 134 | const result = await video.createVideo(info, fileIds, version) 135 | this.logger.info(JAVID, 'processed!') 136 | 137 | return result 138 | } 139 | 140 | /** 141 | * Create file records for file bundle 142 | * 143 | * @param {Object} data files info 144 | * 145 | * @returns {Object} file ids 146 | */ 147 | async createFileRecord (data, version = 1) { 148 | this.logger.debug('Creating file records') 149 | const fileIds = { 150 | metaId: 0, 151 | videoId: 0, 152 | storyboardId: {} 153 | } 154 | 155 | const storageDataList = [ 156 | JSON.stringify({ fileId: data.fileInfo.id }), 157 | JSON.stringify({ fileId: data.videoId }) 158 | ] 159 | 160 | if (version === 1) { 161 | for (const i in data.storyboardList) { 162 | const item = data.storyboardList[i] 163 | storageDataList.push(JSON.stringify({ fileId: item.id })) 164 | } 165 | } 166 | 167 | const result = await file.createFilesRecord(this.id, storageDataList) 168 | 169 | fileIds.metaId = result[JSON.stringify({ fileId: data.fileInfo.id })] 170 | 171 | fileIds.videoId = result[JSON.stringify({ fileId: data.videoId })] 172 | 173 | if (version === 1) { 174 | for (const i in data.storyboardList) { 175 | const item = data.storyboardList[i] 176 | fileIds.storyboardId[i] = result[JSON.stringify({ fileId: item.id })] 177 | } 178 | } 179 | 180 | return fileIds 181 | } 182 | } 183 | 184 | module.exports = googleDrive 185 | -------------------------------------------------------------------------------- /src/module/bookmark.js: -------------------------------------------------------------------------------- 1 | const db = require('./database') 2 | const metadata = require('./metadata') 3 | 4 | class Bookmark { 5 | /** 6 | * Create new bookmark 7 | * 8 | * @param {Int} uid user id 9 | * @param {String} name bookmark name 10 | * 11 | * @returns {Int} bookmark id 12 | */ 13 | async createBookmark (uid, name) { 14 | let result = await db('bookmarks').insert({ 15 | uid, 16 | name, 17 | createTime: (new Date()).getTime(), 18 | updateTime: (new Date()).getTime() 19 | }).select('id') 20 | 21 | result = result[0] 22 | 23 | return result 24 | } 25 | 26 | /** 27 | * Add metadata to bookmark 28 | * 29 | * @param {Int} bookmarkId bookmark id 30 | * @param {Int} metadataId matedata id 31 | * 32 | * @returns {Int} bookmark mapping id 33 | */ 34 | async addMetadata (bookmarkId, metadataId) { 35 | const num = await db('bookmarks_mapping').where('bookmarkId', bookmarkId).where('metadataId', metadataId).count() 36 | 37 | if (num && num[0]['count(*)'] !== 0) return true 38 | 39 | let result = await db('bookmarks_mapping').insert({ 40 | bookmarkId, 41 | metadataId, 42 | updateTime: (new Date()).getTime() 43 | }).select('id') 44 | 45 | result = result[0] 46 | 47 | return result 48 | } 49 | 50 | /** 51 | * Get bookmark info by id 52 | * 53 | * @param {Int} id bookmark id 54 | * @param {Boolean} onlyName only name 55 | * 56 | * @returns {Array} matedata id 57 | */ 58 | async getBookmarkInfo (id, onlyName = false, page = 1, size = 20) { 59 | const bookmarkName = await db('bookmarks').where('id', id).select('*').first() 60 | if (onlyName) { 61 | return { 62 | id: bookmarkName.id, 63 | name: bookmarkName.name 64 | } 65 | } 66 | let metadatas = await db('bookmarks_mapping').where('bookmarkId', id).orderBy('id', 'desc').select('*').paginate({ 67 | perPage: size, 68 | currentPage: page 69 | }) 70 | 71 | const processed = [] 72 | metadatas = metadatas.data 73 | for (const i in metadatas) { 74 | const item = metadatas[i] 75 | processed.push(await metadata.getMetadataById(item.metadataId)) 76 | } 77 | 78 | let total = await db('bookmarks_mapping').where('bookmarkId', id).count() 79 | total = total[0]['count(*)'] 80 | 81 | return { 82 | total, 83 | name: bookmarkName.name, 84 | metadatas: processed 85 | } 86 | } 87 | 88 | /** 89 | * Get number of user's bookmarks 90 | * 91 | * @param {Int} uid user id 92 | */ 93 | async getBookmarkNumByUserId (uid) { 94 | const result = await db('bookmarks').where('uid', uid).count() 95 | 96 | if (result || result[0]) return result[0]['count(*)'] 97 | return result 98 | } 99 | 100 | /** 101 | * Get number of bookmark's items 102 | * 103 | * @param {Int} bookmarkId bookmark id 104 | * 105 | * @returns {Int} 106 | */ 107 | async getBookmarkInfoNum (bookmarkId) { 108 | const result = await db('bookmarks_mapping').where('bookmarkId', bookmarkId).count() 109 | 110 | if (result || result[0]) return result[0]['count(*)'] 111 | return 100 112 | } 113 | 114 | /** 115 | * Remove bookmark 116 | * 117 | * @param {Int} id bookmark id 118 | * 119 | * @returns {Boolean} true 120 | */ 121 | async removeBookmark (id) { 122 | await db('bookmarks').where('id', id).delete() 123 | await db('bookmarks_mapping').where('bookmarkId', id).delete() 124 | 125 | return true 126 | } 127 | 128 | /** 129 | * Remove metadata from bookmark 130 | * 131 | * @param {Int} bookmarkId bookmark id 132 | * @param {Int} metadataId metadata id 133 | * 134 | * @returns {Boolean} 135 | */ 136 | async removeMetadata (bookmarkId, metadataId) { 137 | const result = await db('bookmarks_mapping').where('bookmarkId', bookmarkId).where('metadataId', metadataId).delete() 138 | 139 | if (result) return true 140 | return false 141 | } 142 | 143 | /** 144 | * Check permission of bookmark 145 | * 146 | * @param {Int} uid user id 147 | * @param {Int} bookmarkId bookmark id 148 | * 149 | * @returns {Boolean} 150 | */ 151 | async isOwn (uid, bookmarkId) { 152 | const result = await db('bookmarks').where('uid', uid).where('id', bookmarkId).count() 153 | 154 | if (result[0]['count(*)'] !== 0) return true 155 | return false 156 | } 157 | 158 | /** 159 | * Get bookmark list by user id 160 | * 161 | * @param {Int} uid user id 162 | * @param {Boolean} onlyId only return id list 163 | * 164 | * @returns {Array} bookmark list 165 | */ 166 | async getUserBookmarkList (uid, onlyId = false) { 167 | const result = await db('bookmarks').where('uid', uid).select('*') 168 | 169 | const processed = [] 170 | for (const i in result) { 171 | const item = result[i] 172 | processed.push((onlyId) ? item.id : Object.assign({}, item)) 173 | } 174 | 175 | return processed 176 | } 177 | 178 | /** 179 | * Get bookmark list by metadata id 180 | * 181 | * @param {Int} uid user id 182 | * @param {Int} metadataId metadata id 183 | * 184 | * @returns {Array} bookmark list 185 | */ 186 | async getBookmarkByMetadataId (uid, metadataId) { 187 | const own = await this.getUserBookmarkList(uid, true) 188 | const result = await db('bookmarks_mapping').where('metadataId', metadataId).select('*') 189 | 190 | let processed = new Set() 191 | for (const i in result) { 192 | const item = result[i] 193 | if (!own.includes(item.bookmarkId)) continue 194 | processed.add(item.bookmarkId) 195 | } 196 | 197 | processed = Array.from(processed) 198 | const again = [] 199 | for (const i in processed) { 200 | const res = await this.getBookmarkInfo(processed[i], true) 201 | again.push(res) 202 | } 203 | 204 | return again 205 | } 206 | } 207 | 208 | module.exports = new Bookmark() 209 | -------------------------------------------------------------------------------- /src/api/route/user.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const config = require('./../../module/config') 4 | const user = require('./../../module/user') 5 | const permission = require('./../../module/permission') 6 | 7 | router.get('/getUserList/:page?/:size?', async (req, res) => { 8 | const per = await permission.getUserPermissionGroupInfo(req.uid) 9 | if (!per.rule.admin) { 10 | res.status(403).json({ 11 | code: -1, 12 | msg: 'Access denied', 13 | data: {} 14 | }) 15 | return 16 | } 17 | 18 | let { page, size } = req.params 19 | page = parseInt(page || 1) 20 | size = parseInt(size || 20) 21 | 22 | if (page < 1 || size < 1 || size > 50) { 23 | res.json({ 24 | code: -2, 25 | msg: 'Param error', 26 | data: {} 27 | }) 28 | return 29 | } 30 | 31 | const result = await user.getUserList(page, size) 32 | 33 | res.json({ 34 | code: 0, 35 | msg: 'Success', 36 | data: result 37 | }) 38 | }) 39 | 40 | router.post('/createUser', async (req, res) => { 41 | const per = await permission.getUserPermissionGroupInfo(req.uid) 42 | if (!per.rule.admin) { 43 | res.status(403).json({ 44 | code: -1, 45 | msg: 'Access denied', 46 | data: {} 47 | }) 48 | return 49 | } 50 | 51 | const body = req.body 52 | if (!body || !body.username || !body.password || !body.groupId) { 53 | res.json({ 54 | code: -2, 55 | msg: 'Param error', 56 | data: {} 57 | }) 58 | return 59 | } 60 | 61 | const uid = await user.createUser(body.username, body.password, body.groupId, body.comment || '', body.from || 'Admin insert') 62 | 63 | if (uid === -1) { 64 | res.json({ 65 | code: -2, 66 | msg: 'Username exists', 67 | data: {} 68 | }) 69 | } else { 70 | res.json({ 71 | code: 0, 72 | msg: 'Success', 73 | data: { 74 | uid 75 | } 76 | }) 77 | } 78 | }) 79 | 80 | router.post('/removeUser', async (req, res) => { 81 | const per = await permission.getUserPermissionGroupInfo(req.uid) 82 | if (!per.rule.admin) { 83 | res.status(403).json({ 84 | code: -1, 85 | msg: 'Access denied', 86 | data: {} 87 | }) 88 | return 89 | } 90 | 91 | const body = req.body 92 | if (!body || !body.uid) { 93 | res.json({ 94 | code: -2, 95 | msg: 'Param error', 96 | data: {} 97 | }) 98 | return 99 | } 100 | 101 | const result = await user.removeUser(body.uid) 102 | 103 | res.json({ 104 | code: 0, 105 | msg: 'Success', 106 | data: { 107 | result 108 | } 109 | }) 110 | }) 111 | 112 | router.post('/changeUsername', async (req, res) => { 113 | const body = req.body 114 | if (!body || !body.newUsername) { 115 | res.json({ 116 | code: -2, 117 | msg: 'Param error', 118 | data: {} 119 | }) 120 | return 121 | } 122 | 123 | let uid = req.uid 124 | if (body.uid) { 125 | const per = await permission.getUserPermissionGroupInfo(req.uid) 126 | if (!per.rule.admin) { 127 | res.status(403).json({ 128 | code: -1, 129 | msg: 'Access denied', 130 | data: {} 131 | }) 132 | return 133 | } 134 | uid = body.uid 135 | } else { 136 | const allow = config.get('system.allowChangeUsername') || false 137 | 138 | if (allow === false) { 139 | res.json({ 140 | code: -2, 141 | msg: 'Your can\'t change your username now due to the policy of the site owner', 142 | data: {} 143 | }) 144 | return 145 | } 146 | } 147 | 148 | const result = await user.changeUsername(uid, body.newUsername) 149 | 150 | if (result === -1) { 151 | res.json({ 152 | code: -2, 153 | msg: 'Username exists', 154 | data: {} 155 | }) 156 | } else { 157 | res.json({ 158 | code: 0, 159 | msg: 'Success', 160 | data: { 161 | uid 162 | } 163 | }) 164 | } 165 | }) 166 | 167 | router.post('/changePassword', async (req, res) => { 168 | const body = req.body 169 | if (!body || !body.newPassword) { 170 | res.json({ 171 | code: -2, 172 | msg: 'Param error', 173 | data: {} 174 | }) 175 | return 176 | } 177 | 178 | let uid = req.uid 179 | if (body.uid) { 180 | const per = await permission.getUserPermissionGroupInfo(req.uid) 181 | if (!per.rule.admin) { 182 | res.status(403).json({ 183 | code: -1, 184 | msg: 'Access denied', 185 | data: {} 186 | }) 187 | return 188 | } 189 | uid = body.uid 190 | } 191 | 192 | const result = await user.changePassword(uid, body.newPassword) 193 | 194 | res.json({ 195 | code: 0, 196 | msg: 'Success', 197 | data: { 198 | result 199 | } 200 | }) 201 | }) 202 | 203 | router.post('/changeGroup', async (req, res) => { 204 | const body = req.body 205 | if (!body || !body.newGroup) { 206 | res.json({ 207 | code: -2, 208 | msg: 'Param error', 209 | data: {} 210 | }) 211 | return 212 | } 213 | 214 | let uid = req.uid 215 | if (body.uid) { 216 | const per = await permission.getUserPermissionGroupInfo(req.uid) 217 | if (!per.rule.admin) { 218 | res.status(403).json({ 219 | code: -1, 220 | msg: 'Access denied', 221 | data: {} 222 | }) 223 | return 224 | } 225 | uid = body.uid 226 | } 227 | 228 | const result = await user.changeGroup(uid, body.newGroup) 229 | 230 | res.json({ 231 | code: 0, 232 | msg: 'Success', 233 | data: { 234 | result 235 | } 236 | }) 237 | }) 238 | 239 | router.post('/changeComment', async (req, res) => { 240 | const body = req.body 241 | if (!body || !body.newComment || !body.uid) { 242 | res.json({ 243 | code: -2, 244 | msg: 'Param error', 245 | data: {} 246 | }) 247 | return 248 | } 249 | 250 | const per = await permission.getUserPermissionGroupInfo(req.uid) 251 | if (!per.rule.admin) { 252 | res.status(403).json({ 253 | code: -1, 254 | msg: 'Access denied', 255 | data: {} 256 | }) 257 | return 258 | } 259 | 260 | const result = await user.changeComment(body.uid, body.newComment) 261 | 262 | res.json({ 263 | code: 0, 264 | msg: 'Success', 265 | data: { 266 | result 267 | } 268 | }) 269 | }) 270 | 271 | module.exports = router 272 | -------------------------------------------------------------------------------- /src/module/driver/googleDrive.js: -------------------------------------------------------------------------------- 1 | const { google } = require('googleapis') 2 | const pRetry = require('p-retry') 3 | const cryptoJs = require('crypto-js') 4 | const base64 = require('js-base64').Base64 5 | const randomInt = require('random-int') 6 | 7 | class GoogleDrive { 8 | /** 9 | * Create a instance of Google Drive Driver 10 | * 11 | * @param {Number} id 12 | * @param {Object=} data 13 | */ 14 | constructor (id, data = {}) { 15 | this.logger = require('./../logger')(`Driver[${id}]: Google Drive`) 16 | if (data.oAuth) { 17 | this.oAuth2Client = new google.auth.OAuth2(data.oAuth.client_id, data.oAuth.client_secret, data.oAuth.redirect_uri) 18 | this.oAuth2Client.setCredentials(data.oAuth.token) 19 | } 20 | 21 | if (data.drive) { 22 | this.driveClient = google.drive({ 23 | version: 'v3', 24 | auth: this.oAuth2Client 25 | }) 26 | } 27 | this._data = data 28 | } 29 | 30 | /** 31 | * Authorize with code 32 | * 33 | * @param {Object} data Google Driver configuration 34 | * 35 | * @returns {Object} Google Driver configuration 36 | */ 37 | async authorizeWithCode (data) { 38 | data = data.oAuth 39 | if (!this.oAuth2Client) { 40 | this.oAuth2Client = new google.auth.OAuth2(data.client_id, data.client_secret, data.redirect_uri) 41 | } 42 | 43 | this.logger.debug('Retrieving access token') 44 | return new Promise((resolve, reject) => { 45 | this.oAuth2Client.getToken(data.code, (error, token) => { 46 | if (error) { 47 | this.logger.error('Error retrieving access token', error) 48 | reject(new Error('Error retrieving access token')) 49 | return 50 | } 51 | this.logger.debug('Got access token') 52 | this.logger.debug(token) 53 | this.oAuth2Client.setCredentials(token) 54 | 55 | resolve({ 56 | client_id: data.client_id, 57 | client_secret: data.client_secret, 58 | redirect_uri: data.redirect_uri, 59 | token 60 | }) 61 | }) 62 | }) 63 | } 64 | 65 | /** 66 | * Refresh Google API's access token 67 | * 68 | * @param {Object=} data Google Driver configuration 69 | * 70 | * @returns {Object} Google Driver configuration 71 | */ 72 | async refreshToken (data) { 73 | if (!data) data = this._data 74 | data = data.oAuth 75 | if (!this.oAuth2Client) { 76 | this.oAuth2Client = new google.auth.OAuth2(data.client_id, data.client_secret, data.redirect_uri) 77 | this.oAuth2Client.setCredentials(data.token) 78 | } 79 | 80 | const expiryDate = this.oAuth2Client.credentials.expiry_date 81 | this.logger.debug('Token expiry date', expiryDate) 82 | if (((new Date()).getTime() + 600000) < expiryDate) return 83 | 84 | this.logger.debug('Refreshing access token') 85 | return new Promise((resolve, reject) => { 86 | this.oAuth2Client.refreshAccessToken((error, token) => { 87 | if (error) { 88 | this.logger.error('Error refreshing access token', error) 89 | reject(new Error('Error refreshing access token')) 90 | return 91 | } 92 | 93 | this.logger.debug('Got access token') 94 | this.logger.debug(token) 95 | resolve(token) 96 | }) 97 | }) 98 | } 99 | 100 | /** 101 | * Get file list 102 | * 103 | * @param {String} q Keywords 104 | * @param {String=} fields Selected fields 105 | * @param {Boolean=} full Get all files 106 | * @param {String} orderBy Order of the list 107 | * 108 | * @returns {Array} File list 109 | */ 110 | async getFileList (q, fields, full, orderBy, pageSize) { 111 | full = full || false 112 | 113 | if (!this.checkAuthorizationStatus()) return 114 | 115 | let data = [] 116 | let pageToken 117 | let counter = 1 118 | 119 | this.logger.info(`Getting ${(full) ? 'full ' : ''}file list of keyword`, q) 120 | do { 121 | this.logger.debug(`Getting page ${counter}`) 122 | let params = { 123 | pageSize: pageSize || 1000, 124 | orderBy: orderBy || 'modifiedTime desc', 125 | q, 126 | fields: 'nextPageToken, files(' + (fields || 'id, name, modifiedTime, parents, size') + ')' 127 | } 128 | 129 | if (this._data.drive.type === 'user') { 130 | params = { 131 | ...params, 132 | corpora: 'user', 133 | includeItemsFromAllDrives: false 134 | } 135 | } else { 136 | params = { 137 | ...params, 138 | driveId: this._data.drive.driveId, 139 | corpora: 'drive', 140 | includeItemsFromAllDrives: true, 141 | supportsTeamDrives: true 142 | } 143 | } 144 | 145 | if (pageToken) params.pageToken = pageToken 146 | let res 147 | try { 148 | res = await pRetry(async () => { 149 | const result = await this.driveClient.files.list(params) 150 | 151 | return result 152 | }, { 153 | onFailedAttempt: async (error) => { 154 | this.logger.debug(`Attempt ${error.attemptNumber} failed. There are ${error.retriesLeft} retries left`) 155 | 156 | return new Promise((resolve) => { 157 | setTimeout(() => { 158 | resolve() 159 | }, 10000) 160 | }) 161 | }, 162 | retries: 5 163 | }) 164 | } catch (error) { 165 | this.logger.error('Error while getting dir list', q, error) 166 | return [] 167 | } 168 | res = res.data 169 | if (res.nextPageToken && full) pageToken = res.nextPageToken 170 | else pageToken = null 171 | data = data.concat(res.files) 172 | counter++ 173 | } while (pageToken) 174 | 175 | this.logger.info(`Got ${data.length} files' metadatas`) 176 | return data 177 | } 178 | 179 | /** 180 | * Get file download URL 181 | * 182 | * @param {String} Google Drive file storage data 183 | * 184 | * @returns {String} URL 185 | */ 186 | async getFileURL (storageData) { 187 | if (this._data.encryption && this._data.encryption.secret && this._data.encryption.server) { 188 | const uri = cryptoJs.AES.encrypt(this._data.drive.driveId + '||!||' + storageData.fileId, this._data.encryption.secret).toString() 189 | const server = this._data.encryption.server.split(',') 190 | return server[randomInt(0, server.length - 1)] + '/' + base64.encode(uri) 191 | } 192 | return '' 193 | } 194 | 195 | /** 196 | * Download file by fileId 197 | * 198 | * @param {String} fileId Google Drive fileId 199 | * 200 | * @returns {ArrayBuffer} File buffer 201 | */ 202 | async downloadFile (fileId) { 203 | if (!this.checkAuthorizationStatus()) return 204 | 205 | this.logger.debug('Downloading file', fileId) 206 | 207 | let res 208 | 209 | try { 210 | res = await pRetry(async () => { 211 | let params = { 212 | alt: 'media', 213 | fileId 214 | } 215 | 216 | if (this._data.drive.type === 'user') { 217 | params = { 218 | ...params, 219 | corpora: 'user', 220 | includeItemsFromAllDrives: false 221 | } 222 | } else { 223 | params = { 224 | ...params, 225 | driveId: this._data.drive.driveId, 226 | corpora: 'drive', 227 | includeItemsFromAllDrives: true, 228 | supportsTeamDrives: true 229 | } 230 | } 231 | 232 | const result = await this.driveClient.files.get(params, { 233 | responseType: 'arraybuffer' 234 | }) 235 | 236 | return result 237 | }, { 238 | onFailedAttempt: async (error) => { 239 | this.logger.debug(`Attempt ${error.attemptNumber} failed. There are ${error.retriesLeft} retries left`) 240 | 241 | return new Promise((resolve) => { 242 | setTimeout(() => { 243 | resolve() 244 | }, 20000) 245 | }) 246 | }, 247 | retries: 5 248 | }) 249 | } catch (error) { 250 | this.logger.error('Error while downloading file', fileId, error) 251 | return [] 252 | } 253 | 254 | res = Buffer.from(res.data, 'binary') 255 | return res 256 | } 257 | 258 | /** 259 | * Get class authorization status 260 | * 261 | * @returns {Boolean} status 262 | */ 263 | checkAuthorizationStatus () { 264 | if (!this.oAuth2Client || !this.driveClient) { 265 | this.logger.error('Havn\'t authorize yet.') 266 | return false 267 | } 268 | 269 | return true 270 | } 271 | } 272 | 273 | module.exports = GoogleDrive 274 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | JAVClub 3 |
JAVClub
4 |

5 | 6 | ❗ | **因架构调整,本项目已不再维护并将存档。新项目将支持泛媒体文件管理,相关开发工作将迁移至 [@UsagiHouse](https://github.com/UsagiHouse) 进行,请知悉** 7 | :---: | :--- 8 | ⚠️ | 因 Google Drive 相关服务近期进行转型升级,无限存储空间可能将不再对个人/教育提供,故本项目暂时停止更新。若后续 Google 仍决定以任意一种可承受的方式提供无限存储空间,项目可能将继续更新。若不会继续提供,则项目可能会 archive 或者支持 OneDrive 等其他网盘平台,敬请谅解 9 | 10 | 11 | ## Features 12 | 13 | - 支持在线播放 14 | - 全自动爬取、下载、上传、处理 15 | - 视频、图片数据不占用本地空间 16 | - 代理后速度播放速度可观, 不代理亦可看 17 | - 多用户系统, 可以与的好基友一起穿越 18 | - 可从公开/私有站点下载数据, 多种选择 19 | - Docker 自动部署 20 | - 支持收藏夹 21 | - 支持公告系统 22 | - 支持用户系统 23 | - 支持邀请注册 24 | - ~~面熟的话大概可以直接白嫖~~ 25 | 26 | ## 简介 27 | 28 | 这是一个涩情系列 Repos, 包含三个子项目, 分别是 [fetcher](https://github.com/JAVClub/fetcher)、[web](https://github.com/JAVClub/web) 还有这个项目 29 | 30 | 稍微逛了一下 GitHub, 发现现有的 JAV 数据库都仅限于存储 Metadata(JAV 元数据[车牌号、cover 等等]) 及没啥用的种子信息, 没法做到在线观看, 所以这就是一个集搜集、下载、存储、观看、管理为一体的东西了 31 | 32 | 项目应该已经差不多进入了稳定期, 各种 TODO 应该有空有兴趣了会填坑, bugfixs 正常, issues 回复期在一至两周左右, 还请见谅 33 | 34 | 往下看之前请先确保你已满 18 周岁 35 | 36 | ![Are you 18](https://github.com/JAVClub/core/raw/master/docs/are-you-18.jpg) 37 | 38 | ## TODO 39 | 40 | - [x] 公告栏 41 | - [x] 用户系统 42 | - [x] 邀请注册 43 | 44 | ## DEMO 45 | 46 | > 感谢某位 dalao 为项目提供非官方演示站, 站点地址[在这](https://fucklo.li), 目前开放注册, 数据继承自原演示站(70k+), 欢迎体验 47 | > (附: 不提供在线时间保证, 有问题/赞助请联系[这里](mailto:contact@fucklol.li)) 48 | 49 | ~~因为项目的特殊性就不提供在线 DEMO 了, 仅放一些图片 #SFW~~ 50 | 51 |
52 | 53 | 页面截图 (点击展开) 54 | 55 | ![Home](https://github.com/JAVClub/core/raw/master/docs/Home.png) 56 | 57 | ![Metadata List](https://github.com/JAVClub/core/raw/master/docs/MetadataList.png) 58 | 59 | ![Metadata Info Top](https://github.com/JAVClub/core/raw/master/docs/MetadataInfoTop.png) 60 | 61 | ![Metadata Info Bottom](https://github.com/JAVClub/core/raw/master/docs/MetadataInfoBottom.png) 62 | 63 | ![Bookmark List](https://github.com/JAVClub/core/raw/master/docs/BookmarkList.png) 64 | 65 | ![Bookmark Info](https://github.com/JAVClub/core/raw/master/docs/BookmarkInfo.png) 66 | 67 | ![Tag List](https://github.com/JAVClub/core/raw/master/docs/TagList.png) 68 | 69 | ![Star List](https://github.com/JAVClub/core/raw/master/docs/StarList.png) 70 | 71 | ![Series List](https://github.com/JAVClub/core/raw/master/docs/SeriesList.png) 72 | 73 | ![Profile](https://github.com/JAVClub/core/raw/master/docs/Profile.png) 74 | 75 |
76 | 77 | ## 部署 78 | 79 | 下面的信息可能有一些繁琐枯燥甚至还有错误, 希望还可见谅, 套用某位 dalao 的话来讲就是一劳永逸, 一旦理解了就没什么困难的了 80 | 81 | **Docker 部署方式请[看这里](https://github.com/JAVClub/docker)** 82 | 83 | 部署之前请确保你拥有/完成以下能力/事情: 84 | - 一台有稳定国际互联网的服务器 85 | - Node.js / JavaScript 基础 86 | - 基本的报错阅读能力 87 | - Linux 基础 88 | - 阅读过《[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md)》 89 | - ~~可以克制住自己想把作者往死里揍心情的能力~~ 90 | 91 | 要正常工作的话总共需要部署几样东西, 它们之间关系是这样的: 92 | ``` 93 | fetcher: 抓取种子->推送 qBittorrent 下载->处理->上传 Google Drive 94 | ↑ 95 | | 通过 Google Drive 相互联系 96 | ↓ 97 | core: 读取 Google Drive 文件列表->导入本地数据库 98 | ↑ 99 | | 通过 API 读取数据库中的内容 100 | ↓ 101 | web: 展示信息 102 | ↑ 103 | | 用户请求 104 | | 105 | Vercel: 为 Workers 提供 access token 106 | | 107 | | 302 跳转 108 | ↓ 109 | Workers: 代理 Google Drive 文件及 JAVBus 封面 110 | ``` 111 | 112 | ### Fetcher 部署 113 | 114 | 参照 [JAVClub/fetcher](https://github.com/JAVClub/fetcher) 115 | 116 | ### 代理部署 117 | 118 | 参照 [JAVClub/proxy](https://github.com/JAVClub/proxy) 119 | 120 | ### Core&Web 部署 121 | 122 | #### Docker 123 | 124 | 参照 [core - JAVClub/docker](https://github.com/JAVClub/docker/tree/master/core) 125 | 126 | #### 非 Docker 127 | 128 | ##### 拉取 129 | 130 | 请确保主机已安装 Node.js 环境 (版本 12.0+), 如未安装可使用 nvm 进行安装 131 | ```bash 132 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash 133 | export NVM_DIR="$HOME/.nvm" 134 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm 135 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion 136 | nvm install node # "node" is an alias for the latest version 137 | ``` 138 | 139 | 拉取项目 140 | ```bash 141 | git clone https://github.com/JAVClub/core.git JAVClub_core 142 | cd JAVClub_core 143 | cp config/dev.example.json config/dev.json 144 | npm i 145 | ``` 146 | 147 | ##### 配置文件 148 | 149 |
150 | 151 | 配置文件 (点击展开) 152 | 153 | ```json 154 | { 155 | "system": { 156 | "logLevel": "debug", 157 | "port": 3000, 158 | "path": "/api", 159 | "allowChangeUsername": false, 160 | "userMaxBookmarkNum": 10, 161 | "userMaxBookmarkItemNum": 100, 162 | "corsDomain": [ 163 | "https://yourdomain.com" 164 | ], 165 | "searchParmaNum": 3, 166 | "allowSignup": false, 167 | "defaultGroup": 2 168 | }, 169 | "database": { 170 | "dialect": "mysql", 171 | "connectionLimit": 5, 172 | "host": "mysql", 173 | "port": 3306, 174 | "username": "javclub", 175 | "password": "javclub", 176 | "database": "javclub" 177 | }, 178 | "importer": { 179 | "settings": { 180 | "googleDrive": { 181 | "queueNum": 1 182 | } 183 | }, 184 | "cron": [ 185 | { 186 | "driveId": 1, 187 | "interval": 36000000, 188 | "doFull": true 189 | } 190 | ] 191 | }, 192 | "proxy": [ 193 | "https://proxy.xiaolin.in/" 194 | ] 195 | } 196 | ``` 197 |
198 | 199 | - **system** 200 | - path: API 监听的路径 201 | - corsDomain: cors 头允许的域名 202 | - searchParmaNum: 搜索允许的关键词数量(以空格分隔) 203 | - defaultGroup: 用户通过直接注册进入的权限组 ID (保持默认即可) 204 | - **importer** 205 | - settings.googleDrive.queueNum: (Int) Importer 导入时队列并行数 206 | - cron[].driverId: (Int) 数据库 `drivers` 表中条目的 ID 207 | - cron[].interval: (Int) 每隔多少毫秒 扫描一次这个云端硬盘 208 | - cron[].doFull: (Boolean) 启动程序后第一次运行时是否扫描云盘全部内容 (建议第一次导入完成后关闭) 209 | - **proxy** (Array) 用于代理 Metadata Cover 及 Star Cover 的反代 URL (请求格式: `https://your.img.proxy/https://url.to/imgage.png`) 210 | 211 | 按照提示修改 `config/dev.json` 并更改相关配置即可 212 | - `system` 部分若无需更改保持默认即可 213 | - `database` 部分请修改 `host` `port` `username` `password` `database` 为你自己的信息 214 | - `cron` 部分的相关设定可以**暂时不用填写**, 下文会有详细讲解 215 | - `proxy` 字段, 如果不想部署图片代理的话也可以直接填写 `[""]` 216 | 217 | ##### 数据库 218 | 219 | ~~因程序不打算弄太复杂, 所以没有安装界面, 请自行导入数据表~~ 220 | 221 | 在最新版本中终于用上了 migration, 所以现在数据表在启动时会自动创建, 默认的用户名 / 密码为 `admin` / `admin`, 请及时修改 222 | 223 | ##### 配置 Google Drive 相关 224 | 225 | core 中的数据来源是 fetcher 上传至 Google Drive 中的数据, 请在使用前 1-2 天部署好 fetcher 以获取足够的数据 (当然你要是想部署完 core 再部署 fetcher 也是没问题的) 226 | 227 | 首先要做的是往数据库里添加有关 Google Drive 的信息, 样例 SQL 命令如下 228 | ```sql 229 | INSERT INTO `drivers` (`id`, `name`, `driverType`, `driverData`, `isEnable`, `createTime`, `updateTime`) VALUES 230 | (1, 'My first drive', 'gd', '{\"oAuth\":{\"client_id\":\"【your_client_here】\",\"client_secret\":\"【your_client_secret_here】\",\"redirect_uri\":\"urn:ietf:wg:oauth:2.0:oob\",\"token\":{\"access_token\":\"【your_access_token_here_optional】\",\"refresh_token\":\"【your_refresh_token_here】\",\"scope\":\"https://www.googleapis.com/auth/drive\",\"token_type\":\"Bearer\",\"expiry_date\":1583679345619}},\"drive\":{\"driveId\":\"【your_drive_or_folder_id_here】\"},\"encryption\":{\"secret\":\"【path_ase_secret】\",\"server\":\"【your_gd_proxy_server_here】"}}', 1, '1583679345619', '1583679345619'); 231 | ``` 232 | 233 | `driverData` 是这部分的核心, 看起来挺乱的, 这里给一个格式化后的方便理解 234 | ```json 235 | { 236 | "oAuth":{ 237 | "client_id":"xxx.apps.googleusercontent.com", 238 | "client_secret":"", 239 | "redirect_uri":"urn:ietf:wg:oauth:2.0:oob", 240 | "token":{ 241 | "access_token":"", 242 | "refresh_token":"", 243 | "scope":"https://www.googleapis.com/auth/drive", 244 | "token_type":"Bearer", 245 | "expiry_date":1583679345619 246 | } 247 | }, 248 | "drive":{ 249 | "driveId":"987b3d98q7deuiedsr", 250 | "type": "shared" 251 | }, 252 | "encryption":{ 253 | "secret":"secret", 254 | "server":"https://proxy.abc.workers.dev,https://proxy.def.workers.dev" 255 | } 256 | } 257 | ``` 258 | - oAuth 中的顾名思义就是 Google API 的鉴权信息, 按照你的凭证填写即可 259 | - 凭证相关可使用 [GoIndex Code Builder](https://install.achirou.workers.dev/zh) 来方便地取得, 将生成代码中的 `client_id`、`client_secret`、`refresh_token` 复制到此处即可, 其余位置可留空 260 | - drive 261 | - driveId 是你的云端硬盘 ID, 也就是云端硬盘根目录浏览器地址栏的那一长串东西 262 | - type[optional] 可选 `user` 或 `shared`, 选择 `user` 时无需填写 `driveId`, 代表 `我的云端硬盘` 263 | - encryption 是给 Workers 使用的选项 264 | - secret 请随便填写串字符, 部署 Workers 时使用的 `password` 请与此处的保持一致 265 | - server 是你部署的 Workers 的地址, 多个地址用 `,` 隔开 266 | 267 | 更改完后将上面一段 JSON 复制到[这里](json.cn)压缩后照本节开头格式插入数据表即可 268 | 269 | 下一步就是要告诉程序你添加了这个硬盘并且希望扫描/导入这个硬盘中的内容 270 | 271 | 还记得[上文](#配置文件)中提到的 `cron` 部分吗? 那里的 `id` 便是这里数据表中自动生成的 `id` 272 | 273 | 那么就只需要在 `dev.json` 中的 `cron` 字段按中所述添加相应内容即可 274 | 275 | 到现在 core 应该已经配置完成并可以工作了 276 | 277 | ##### 配置 WebUI 278 | 279 | 到现在只剩下 WebUI 程序就可以正常工作了, 为了正常工作需要将 core 的 `/api` 路径代理到你域名下的 `/api` 路径并将静态资源放置于该域名对应目录的根目录下, 请使用你熟悉的 HTTP 服务端软件来执行此操作(如 Nginx, Caddy 等) 280 | 281 | 首先是拉取并构建 Web UI 282 | ```bash 283 | git clone https://github.com/JAVClub/web.git JAVClub_web 284 | cd JAVClub_web 285 | cp src/config.example.js src/config.js 286 | npm i && npm run build 287 | ``` 288 | 289 | 运行完成之后前端资源就已经构建完成了, 位于 `./dist` 目录下 290 | 这时候只需要在服务端软件中将除 `/api` 以外的请求重定向至 `./dist` 目录即可 291 | 292 | ##### 启动: 293 | 294 | ```bash 295 | NODE_ENV=dev node src/app.js 296 | # 以及你服务端的启动命令 297 | ``` 298 | 299 | 没有意外的话现在 Web UI 和 API 服务器应该已经启动并正常工作了, 可以观察一下输出日志中有没有错误 (如果有务必将错误日志提交至 Issue 300 | 301 | 如果有任何不明白的欢迎开 issue 提问 302 | 303 | ### 完成 304 | 305 | 现在 JAVClub 已经成功运行起来了 306 | 307 | 那么在这里祝你身体健康 308 | 309 | ## 其余配置 310 | 311 | ### 权限组 312 | 313 | 新版本新增了权限系统, 数据库由 `id` `name` `rule` `time` 四个部分组成 314 | 315 | 其中 `id` 是权限组 ID, `name` 是权限组名, `rule` 是权限组的权限列表, 为 JSON 格式, 如下所示 316 | ```json 317 | { 318 | "admin":true, // 是否为管理员 319 | "title":"Admin", 320 | "banned":false, // 是否被封禁 321 | "invitationNum":-1, // 可以创建的邀请码数量 322 | "invitationGroup":2 // 邀请码使用者注册到的权限组 323 | } 324 | ``` 325 | 326 | 程序启动时会自动创建 `Admin Group` `User Group` `Banned Group` 三个组, 可按需调整参数 327 | 328 | ## 后续 329 | 330 | 先感谢看完这篇废话连篇的使用文档, 有很多东西可能没有说明白, 如果有问题请尽管开 IS 来轰炸我吧 331 | 332 | 正常来讲现在整套系统应该已经在正常工作了, 如果没有请再次检查是否漏掉了任何一个步骤 333 | 334 | ## FAQ 335 | 336 | - 遇到一大堆问题没办法解决 337 | 338 | 可以先参考一下 [core#11](https://github.com/JAVClub/core/issues/11) [core#12](https://github.com/JAVClub/core/issues/12) [fetcher#3](https://github.com/JAVClub/fetcher/issues/3#issuecomment-623198549) 339 | 这里是被踩的最多的坑, 可以看看有没有自己遇到的问题 340 | 341 | - Docker 部署的相关问题 342 | 343 | 有关 Docker 部署的任何问题请提交 Issue 或者直接发送邮件询问 344 | 345 | - 没有 M-Team 的账号怎么办 346 | 347 | 现在重写后的 fetcher 也已经支持 OneJAV 了, 所以不需要任何账号都可以正常使用了 348 | 349 | - 这玩意儿真的有人成功部署过吗 350 | 351 | 说实话我也不知道, 我已经尽最大努力简化安装过程&写说明文档了, 如果还是有不懂的可以提交 Issue 352 | 353 | ## 免责声明 354 | 355 | 本程序仅供学习了解, 请于下载后 24 小时内删除, 不得用作任何商业用途, 文字、数据及图片均有所属版权, 如转载须注明来源 356 | 357 | 使用本程序必循遵守部署服务器所在地、所在国家和用户所在国家的法律法规, 程序作者不对使用者任何不当行为负责 358 | -------------------------------------------------------------------------------- /migrations/20200720090000-init.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.createTable('bookmarks', { 6 | id: { 7 | type: Sequelize.INTEGER.UNSIGNED, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | allowNull: false, 11 | unique: true 12 | }, 13 | uid: { 14 | type: Sequelize.INTEGER.UNSIGNED, 15 | allowNull: false, 16 | unique: true 17 | }, 18 | name: { 19 | type: Sequelize.STRING, 20 | allowNull: false 21 | }, 22 | createTime: { 23 | type: Sequelize.STRING(20), 24 | allowNull: false 25 | }, 26 | updateTime: { 27 | type: Sequelize.STRING(20), 28 | allowNull: false 29 | } 30 | }) 31 | 32 | await queryInterface.createTable('bookmarks_mapping', { 33 | id: { 34 | type: Sequelize.INTEGER.UNSIGNED, 35 | primaryKey: true, 36 | autoIncrement: true, 37 | allowNull: false, 38 | unique: true 39 | }, 40 | bookmarkId: { 41 | type: Sequelize.INTEGER.UNSIGNED, 42 | allowNull: false 43 | }, 44 | metadataId: { 45 | type: Sequelize.INTEGER.UNSIGNED, 46 | allowNull: false 47 | }, 48 | updateTime: { 49 | type: Sequelize.STRING(20), 50 | allowNull: false 51 | } 52 | }) 53 | 54 | queryInterface.addIndex( 55 | 'bookmarks_mapping', 56 | ['bookmarkId', 'metadataId'], 57 | { 58 | indicesType: 'UNIQUE' 59 | } 60 | ) 61 | 62 | await queryInterface.createTable('drivers', { 63 | id: { 64 | type: Sequelize.TINYINT.UNSIGNED, 65 | primaryKey: true, 66 | autoIncrement: true, 67 | allowNull: false, 68 | unique: true 69 | }, 70 | name: { 71 | type: Sequelize.STRING(30), 72 | allowNull: false, 73 | unique: true 74 | }, 75 | driverType: { 76 | type: Sequelize.STRING(10), 77 | allowNull: false 78 | }, 79 | driverData: { 80 | type: Sequelize.STRING(2048), 81 | allowNull: false 82 | }, 83 | isEnable: { 84 | type: Sequelize.TINYINT, 85 | allowNull: false 86 | }, 87 | createTime: { 88 | type: Sequelize.STRING(20), 89 | allowNull: false 90 | }, 91 | updateTime: { 92 | type: Sequelize.STRING(20), 93 | allowNull: false 94 | } 95 | }) 96 | 97 | await queryInterface.createTable('files', { 98 | id: { 99 | type: Sequelize.INTEGER.UNSIGNED, 100 | primaryKey: true, 101 | autoIncrement: true, 102 | allowNull: false, 103 | unique: true 104 | }, 105 | driverId: { 106 | type: Sequelize.TINYINT.UNSIGNED, 107 | allowNull: false 108 | }, 109 | storageData: { 110 | type: Sequelize.STRING(128), 111 | allowNull: false 112 | }, 113 | updateTime: { 114 | type: Sequelize.STRING(20), 115 | allowNull: false 116 | } 117 | }) 118 | 119 | await queryInterface.createTable('ignore', { 120 | id: { 121 | type: Sequelize.INTEGER.UNSIGNED, 122 | primaryKey: true, 123 | autoIncrement: true, 124 | allowNull: false, 125 | unique: true 126 | }, 127 | data: { 128 | type: Sequelize.STRING(30), 129 | allowNull: false, 130 | unique: true 131 | } 132 | }) 133 | 134 | await queryInterface.createTable('metadatas', { 135 | id: { 136 | type: Sequelize.INTEGER.UNSIGNED, 137 | primaryKey: true, 138 | autoIncrement: true, 139 | allowNull: false, 140 | unique: true 141 | }, 142 | title: { 143 | type: Sequelize.STRING(128), 144 | allowNull: false, 145 | unique: true 146 | }, 147 | companyName: { 148 | type: Sequelize.STRING(15), 149 | allowNull: false 150 | }, 151 | companyId: { 152 | type: Sequelize.STRING(15), 153 | allowNull: false 154 | }, 155 | posterFileURL: { 156 | type: Sequelize.STRING(128), 157 | allowNull: false 158 | }, 159 | version: { 160 | type: Sequelize.TINYINT.UNSIGNED, 161 | allowNull: false 162 | }, 163 | screenshotFilesURL: { 164 | type: Sequelize.TEXT, 165 | allowNull: false 166 | }, 167 | releaseDate: { 168 | type: Sequelize.STRING(15), 169 | allowNull: false 170 | }, 171 | updateTime: { 172 | type: Sequelize.STRING(20), 173 | allowNull: false 174 | } 175 | }) 176 | 177 | queryInterface.addIndex( 178 | 'metadatas', 179 | ['companyName', 'companyId'], 180 | { 181 | indicesType: 'UNIQUE' 182 | } 183 | ) 184 | 185 | await queryInterface.createTable('series', { 186 | id: { 187 | type: Sequelize.INTEGER.UNSIGNED, 188 | primaryKey: true, 189 | autoIncrement: true, 190 | allowNull: false, 191 | unique: true 192 | }, 193 | name: { 194 | type: Sequelize.STRING(128), 195 | allowNull: false, 196 | unique: true 197 | }, 198 | updateTime: { 199 | type: Sequelize.STRING(20), 200 | allowNull: false 201 | } 202 | }) 203 | 204 | await queryInterface.createTable('series_mapping', { 205 | id: { 206 | type: Sequelize.INTEGER.UNSIGNED, 207 | primaryKey: true, 208 | autoIncrement: true, 209 | allowNull: false, 210 | unique: true 211 | }, 212 | metadataId: { 213 | type: Sequelize.INTEGER.UNSIGNED, 214 | allowNull: false 215 | }, 216 | seriesId: { 217 | type: Sequelize.INTEGER.UNSIGNED, 218 | allowNull: false 219 | }, 220 | updateTime: { 221 | type: Sequelize.STRING(20), 222 | allowNull: false 223 | } 224 | }) 225 | 226 | queryInterface.addIndex( 227 | 'series_mapping', 228 | ['metadataId', 'seriesId'], 229 | { 230 | indicesType: 'UNIQUE' 231 | } 232 | ) 233 | 234 | await queryInterface.createTable('stars', { 235 | id: { 236 | type: Sequelize.INTEGER.UNSIGNED, 237 | primaryKey: true, 238 | autoIncrement: true, 239 | allowNull: false, 240 | unique: true 241 | }, 242 | name: { 243 | type: Sequelize.STRING(128), 244 | allowNull: false, 245 | unique: true 246 | }, 247 | photoURL: { 248 | type: Sequelize.STRING(256), 249 | allowNull: false 250 | }, 251 | updateTime: { 252 | type: Sequelize.STRING(20), 253 | allowNull: false 254 | } 255 | }) 256 | 257 | await queryInterface.createTable('stars_mapping', { 258 | id: { 259 | type: Sequelize.INTEGER.UNSIGNED, 260 | primaryKey: true, 261 | autoIncrement: true, 262 | allowNull: false, 263 | unique: true 264 | }, 265 | metadataId: { 266 | type: Sequelize.INTEGER.UNSIGNED, 267 | allowNull: false 268 | }, 269 | starId: { 270 | type: Sequelize.INTEGER.UNSIGNED, 271 | allowNull: false 272 | }, 273 | updateTime: { 274 | type: Sequelize.STRING(20), 275 | allowNull: false 276 | } 277 | }) 278 | 279 | queryInterface.addIndex( 280 | 'stars_mapping', 281 | ['metadataId', 'starId'], 282 | { 283 | indicesType: 'UNIQUE' 284 | } 285 | ) 286 | 287 | await queryInterface.createTable('tags', { 288 | id: { 289 | type: Sequelize.INTEGER.UNSIGNED, 290 | primaryKey: true, 291 | autoIncrement: true, 292 | allowNull: false, 293 | unique: true 294 | }, 295 | name: { 296 | type: Sequelize.STRING(128), 297 | allowNull: false, 298 | unique: true 299 | }, 300 | updateTime: { 301 | type: Sequelize.STRING(20), 302 | allowNull: false 303 | } 304 | }) 305 | 306 | await queryInterface.createTable('tags_mapping', { 307 | id: { 308 | type: Sequelize.INTEGER.UNSIGNED, 309 | primaryKey: true, 310 | autoIncrement: true, 311 | allowNull: false, 312 | unique: true 313 | }, 314 | metadataId: { 315 | type: Sequelize.INTEGER.UNSIGNED, 316 | allowNull: false 317 | }, 318 | tagId: { 319 | type: Sequelize.INTEGER.UNSIGNED, 320 | allowNull: false 321 | }, 322 | updateTime: { 323 | type: Sequelize.STRING(20), 324 | allowNull: false 325 | } 326 | }) 327 | 328 | queryInterface.addIndex( 329 | 'tags_mapping', 330 | ['metadataId', 'tagId'], 331 | { 332 | indicesType: 'UNIQUE' 333 | } 334 | ) 335 | 336 | await queryInterface.createTable('users', { 337 | id: { 338 | type: Sequelize.INTEGER.UNSIGNED, 339 | primaryKey: true, 340 | autoIncrement: true, 341 | allowNull: false, 342 | unique: true 343 | }, 344 | username: { 345 | type: Sequelize.STRING(32), 346 | allowNull: false, 347 | unique: true 348 | }, 349 | password: { 350 | type: Sequelize.STRING(61).BINARY, 351 | allowNull: false 352 | }, 353 | token: { 354 | type: Sequelize.STRING(64), 355 | allowNull: false, 356 | unique: true 357 | }, 358 | comment: { 359 | type: Sequelize.STRING(128), 360 | allowNull: false 361 | }, 362 | createTime: { 363 | type: Sequelize.STRING(20), 364 | allowNull: false 365 | }, 366 | lastSeen: { 367 | type: Sequelize.STRING(20), 368 | allowNull: false 369 | } 370 | }) 371 | 372 | await queryInterface.createTable('videos', { 373 | id: { 374 | type: Sequelize.INTEGER.UNSIGNED, 375 | primaryKey: true, 376 | autoIncrement: true, 377 | allowNull: false, 378 | unique: true 379 | }, 380 | metadataId: { 381 | type: Sequelize.INTEGER.UNSIGNED, 382 | allowNull: false 383 | }, 384 | videoFileId: { 385 | type: Sequelize.INTEGER.UNSIGNED, 386 | allowNull: false, 387 | unique: true 388 | }, 389 | infoFileId: { 390 | type: Sequelize.INTEGER.UNSIGNED, 391 | allowNull: false 392 | }, 393 | videoMetadata: { 394 | type: Sequelize.STRING(512), 395 | allowNull: false 396 | }, 397 | storyboardFileIdSet: { 398 | type: Sequelize.STRING(1024), 399 | allowNull: false 400 | }, 401 | isHiden: { 402 | type: Sequelize.TINYINT.UNSIGNED, 403 | allowNull: false, 404 | defaultValue: 0 405 | }, 406 | version: { 407 | type: Sequelize.TINYINT.UNSIGNED, 408 | allowNull: false, 409 | defaultValue: 1 410 | }, 411 | updateTime: { 412 | type: Sequelize.STRING(20), 413 | allowNull: false 414 | } 415 | }) 416 | 417 | queryInterface.addIndex( 418 | 'videos', 419 | ['id', 'metadataId'], 420 | { 421 | indicesType: 'UNIQUE' 422 | } 423 | ) 424 | }, 425 | 426 | down: async (queryInterface, Sequelize) => { 427 | queryInterface.dropAllTables() 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /src/module/metadata.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const fetch = require('node-fetch') 3 | const pRetry = require('p-retry') 4 | const parser = new (require('dom-parser'))() 5 | const logger = require('./logger')('Module: MetaData') 6 | const db = require('./database') 7 | const cache = require('./cache') 8 | const file = require('./file') 9 | const ignore = require('./ignore') 10 | const config = require('./config') 11 | 12 | class Metadata { 13 | /** 14 | * Get metadata info by id 15 | * 16 | * @param {Int} id metadata id 17 | * 18 | * @returns {Object} metadata info 19 | */ 20 | async getMetadataById (id) { 21 | logger.debug('Get metadata info, id', id) 22 | let result = await db('metadatas').where('id', id).select('*').first() 23 | logger.debug('Got result', result) 24 | 25 | if (!result) return null 26 | 27 | result = await this._processMetadataList([result]) 28 | result = result[0] 29 | 30 | return result 31 | } 32 | 33 | /** 34 | * Get metadata list 35 | * 36 | * @param {Int} page page number 37 | * @param {Int} size page size 38 | * 39 | * @returns {Array} metadata list 40 | */ 41 | async getMetadataList (page, size) { 42 | let result = await db('metadatas').orderBy('releaseDate', 'desc').select('*').paginate({ 43 | perPage: size, 44 | currentPage: page 45 | }) 46 | 47 | let total = await db('metadatas').count() 48 | total = total[0]['count(*)'] 49 | 50 | result = result.data 51 | if (!result) return [] 52 | 53 | const processed = await this._processMetadataList(result) 54 | 55 | return { 56 | total, 57 | data: processed 58 | } 59 | } 60 | 61 | /** 62 | * Get metadata list by meta id 63 | * 64 | * @param {String} type meta type(tag, series, star) 65 | * @param {Int} metaId meta id 66 | * @param {Int} page page number 67 | * @param {Int} size page size 68 | * 69 | * @returns {Array} metadata list 70 | */ 71 | async getMetadataListByMetaId (type, metaId, page, size) { 72 | const mapping = this._getTypeMapping(type) 73 | 74 | let result = await db(`${mapping.type}_mapping`).where(mapping.column, metaId).orderBy('id', 'desc').select('metadataId').paginate({ 75 | perPage: size, 76 | currentPage: page 77 | }) 78 | 79 | let total = await db(`${mapping.type}_mapping`).where(mapping.column, metaId).count() 80 | total = total[0]['count(*)'] 81 | 82 | result = result.data 83 | if (!result) return [] 84 | 85 | const processed = [] 86 | for (const i in result) { 87 | const metadataId = result[i].metadataId 88 | 89 | const res = await this.getMetadataById(metadataId) 90 | if (res) processed.push(res) 91 | } 92 | 93 | return { 94 | total, 95 | data: processed, 96 | metaInfo: await this.getMetaInfoByMetaId(this._getTypeMapping(type).type, metaId) 97 | } 98 | } 99 | 100 | /** 101 | * Get or create metadata id 102 | * 103 | * @param {String} JAVID in the formal of XXX-001 104 | * 105 | * @returns {Int} metadata id 106 | */ 107 | async getMetadataId (JAVID, version = 1, JAVmetadata) { 108 | logger.info('Creating JAV metadata record', JAVID) 109 | 110 | try { 111 | const metadataId = await db('metadatas').where('companyName', JAVID.split('-')[0]).where('companyId', JAVID.split('-')[1]).first() 112 | if (metadataId && metadataId.id) { 113 | return metadataId.id 114 | } else { 115 | let JAVinfo 116 | 117 | switch (version) { 118 | case 1: 119 | JAVinfo = await this.fetchNew(JAVID) 120 | logger.debug('JAVinfo', JAVinfo) 121 | 122 | if (!JAVinfo || !JAVinfo.tags.length) { 123 | logger.info('Invalid info', JAVinfo) 124 | await ignore.addIgnore(JAVID) 125 | return 0 126 | } 127 | break 128 | 129 | case 2: 130 | if (!JAVmetadata || !JAVmetadata.tags.length) { 131 | logger.info('Invalid version 2 JAV metadata', JAVmetadata) 132 | return 0 133 | } 134 | 135 | JAVinfo = JAVmetadata 136 | break 137 | 138 | default: 139 | logger.info(`Unknown version '${version}'`) 140 | return 0 141 | } 142 | 143 | const anotherMetadataId = await db('metadatas').where('title', JAVinfo.title).first() 144 | if (anotherMetadataId && anotherMetadataId.id) { 145 | return anotherMetadataId.id 146 | } 147 | 148 | const dbData = { 149 | title: JAVinfo.title, 150 | companyName: JAVID.split('-')[0], 151 | companyId: JAVID.split('-')[1], 152 | posterFileURL: JAVinfo.cover, 153 | releaseDate: JAVinfo.releaseDate, 154 | updateTime: (new Date()).getTime(), 155 | version 156 | } 157 | 158 | if (version === 2) dbData.screenshotFilesURL = JAVinfo.screenshots 159 | else dbData.screenshotFilesURL = [] 160 | 161 | dbData.screenshotFilesURL = JSON.stringify(dbData.screenshotFilesURL) 162 | 163 | let metadataId = await db('metadatas').insert(dbData).select('id') 164 | metadataId = metadataId[0] 165 | 166 | const promises = [] 167 | 168 | if (JAVinfo.series) promises.push(this.attachMeta('series', metadataId, JAVinfo.series, null)) 169 | 170 | if (!JAVinfo.stars.length) { 171 | JAVinfo.stars = [ 172 | { 173 | name: '素人', 174 | img: 'https://pics.dmm.co.jp/mono/actjpgs/nowprinting.gif' 175 | } 176 | ] 177 | } 178 | 179 | for (const item of _.uniqBy(JAVinfo.stars, 'name')) { 180 | promises.push(this.attachMeta('star', metadataId, item.name, item.img)) 181 | } 182 | 183 | for (const item of (new Set(JAVinfo.tags))) { 184 | promises.push(this.attachMeta('tag', metadataId, item, null)) 185 | } 186 | 187 | await (Promise.all(promises).catch((error) => { 188 | logger.error(error) 189 | })) 190 | 191 | logger.debug('Finished attching metas') 192 | 193 | return metadataId 194 | } 195 | } catch (error) { 196 | logger.error('Error while creating records', error) 197 | } 198 | } 199 | 200 | /** 201 | * Fetch JAV info from javbus.com 202 | * 203 | * @param {String} JAVID JAV id, in the formal of 'XXX-001' 204 | * 205 | * @returns {Promise} JAV info 206 | */ 207 | async fetchNew (JAVID) { 208 | logger.debug('Request URL', 'https://www.javbus.com/ja/' + JAVID) 209 | const result = await pRetry(async () => { 210 | const res = await fetch('https://www.javbus.com/ja/' + JAVID, { 211 | headers: { 212 | 'Cache-Control': 'max-age=0', 213 | Host: 'www.javbus.com', 214 | Referer: 'https://www.javbus.com', 215 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36' 216 | }, 217 | timeout: 7000 218 | }).then((res) => res.text()) 219 | 220 | return res 221 | }, { 222 | onFailedAttempt: async (error) => { 223 | logger.debug(`Attempt ${error.attemptNumber} failed. There are ${error.retriesLeft} retries left`) 224 | 225 | return new Promise((resolve) => { 226 | setTimeout(() => { 227 | resolve() 228 | }, 20000) 229 | }) 230 | }, 231 | retries: 5 232 | }) 233 | 234 | logger.debug('Result length', result.length) 235 | 236 | const dom = parser.parseFromString(result) 237 | 238 | const data = { 239 | title: '', 240 | cover: '', 241 | studio: '', 242 | series: '', 243 | tags: [], 244 | stars: [], 245 | releaseDate: '' 246 | } 247 | 248 | if (!dom.getElementsByClassName('info')[0]) { 249 | logger.debug('JAV not found') 250 | return 251 | } 252 | 253 | const a = dom.getElementsByClassName('info')[0].getElementsByTagName('a') 254 | for (const i in a) { 255 | const item = a[i] 256 | const at = item.attributes 257 | for (const x in at) { 258 | const attr = at[x] 259 | if (attr.name === 'href') { 260 | const v = attr.value 261 | if (!data.studio && v.indexOf('/ja/studio/') !== -1) { 262 | logger.debug(JAVID, 'Get studio info', item.textContent) 263 | data.studio = item.textContent 264 | } else if (!data.series && v.indexOf('/ja/series/') !== -1) { 265 | logger.debug(JAVID, 'Get series info', item.textContent) 266 | data.series = item.textContent 267 | } else if (v.indexOf('/ja/genre/') !== -1) { 268 | logger.debug(JAVID, 'Get tag info', item.textContent) 269 | data.tags.push(item.textContent) 270 | } 271 | } 272 | } 273 | } 274 | 275 | const imgs = dom.getElementsByClassName('movie')[0].getElementsByTagName('img') 276 | for (const i in imgs) { 277 | const item = imgs[i] 278 | const attrs = item.attributes 279 | if (attrs[0] && (attrs[0].value.indexOf('/actress/') !== -1 || attrs[0].value.indexOf('nowprinting') !== -1)) { 280 | logger.debug(JAVID, 'Get star name', attrs[1].value.trim()) 281 | data.stars.push({ 282 | name: attrs[1].value.trim(), 283 | img: attrs[0].value.trim() 284 | }) 285 | } else if (attrs[0] && (attrs[0].value.indexOf('/cover/') !== -1 || attrs[0].value.indexOf('digital/video') !== -1)) { 286 | logger.debug(JAVID, 'Get JAV name', attrs[1].value) 287 | data.title = attrs[1].value 288 | logger.debug(JAVID, 'Get JAV cover', attrs[0].value) 289 | data.cover = attrs[0].value 290 | } 291 | } 292 | 293 | const p = dom.getElementsByClassName('info')[0].getElementsByTagName('p') 294 | for (const i in p) { 295 | if (data.releaseDate) continue 296 | const item = p[i] 297 | if (item.firstChild && item.firstChild.textContent.indexOf('発売日:') !== -1) { 298 | logger.debug(JAVID, 'Get JAV release date', item.lastChild.textContent.trim()) 299 | data.releaseDate = item.lastChild.textContent.trim() 300 | } 301 | } 302 | 303 | logger.debug(JAVID, data) 304 | 305 | return data 306 | } 307 | 308 | /** 309 | * Get meta list 310 | * 311 | * @param {String} type meta type 312 | * @param {Int} page page number 313 | * @param {Int} size page size 314 | * 315 | * @returns {Array} meta list 316 | */ 317 | async getMetaList (type, page, size) { 318 | let result = await db(type).select('*').paginate({ 319 | perPage: size, 320 | currentPage: page 321 | }) 322 | 323 | let total = await db(type).count() 324 | total = total[0]['count(*)'] 325 | 326 | result = result.data 327 | if (!result) return [] 328 | 329 | const processed = [] 330 | for (const i in result) { 331 | const item = result[i] 332 | if (type === 'stars') item.photoURL = file.getProxyPrefix() + item.photoURL 333 | processed.push(Object.assign({}, item)) 334 | } 335 | 336 | return { 337 | total, 338 | data: processed 339 | } 340 | } 341 | 342 | /** 343 | * Get or create multiple types of metas' id 344 | * 345 | * @param {String} type value can be: tags, stars, series 346 | * @param {String} name name 347 | * @param {String=} photoURL photo URL 348 | * 349 | * @returns {Int} id 350 | */ 351 | async getMetaId (type, name, photoURL) { 352 | try { 353 | const result = await db(`${type}`).where('name', name).first() 354 | if (result) { 355 | logger.debug(`[${type}] record for`, name, result) 356 | return result.id 357 | } else { 358 | logger.debug(`[${type}] record for`, name, 'not found, create one') 359 | 360 | const data = { 361 | name, 362 | updateTime: (new Date()).getTime() 363 | } 364 | 365 | if (photoURL) data.photoURL = photoURL 366 | 367 | let id = await db(`${type}`).insert(data).select('id') 368 | id = id[0] 369 | 370 | logger.debug(`[${type}] record for`, name, 'created,', id) 371 | return id 372 | } 373 | } catch (error) { 374 | logger.error('Error while creating a record', error) 375 | throw error 376 | } 377 | } 378 | 379 | /** 380 | * Get meta list(tags,stars,series) by metadata id 381 | * 382 | * @param {Int} id metadata id 383 | * 384 | * @returns {Object} meta list 385 | */ 386 | async getMetaByMetadataId (id) { 387 | const metas = { 388 | tags: [], 389 | stars: [], 390 | series: null 391 | } 392 | 393 | let result 394 | result = await db('tags_mapping').where('metadataId', id).select('*') 395 | if (result) { 396 | for (const i in result) { 397 | metas.tags.push((await this.getMetaInfoByMetaId('tags', result[i].tagId))) 398 | } 399 | } 400 | 401 | result = await db('stars_mapping').where('metadataId', id).select('*') 402 | if (result) { 403 | for (const i in result) { 404 | metas.stars.push(await this.getMetaInfoByMetaId('stars', result[i].starId)) 405 | } 406 | } 407 | 408 | result = await db('series_mapping').where('metadataId', id).select('*').first() 409 | if (result) { 410 | metas.series = (await this.getMetaInfoByMetaId('series', result.seriesId)) 411 | } 412 | 413 | return metas 414 | } 415 | 416 | /** 417 | * Get meta info by meta id 418 | * 419 | * @param {String} type meta type, tags/stars/series 420 | * @param {Int} id meta id 421 | * 422 | * @returns {Object} meta info 423 | */ 424 | async getMetaInfoByMetaId (type, id) { 425 | const result = await cache(`getMeta_${type}_${id}`, async () => { 426 | const res = await db(type).where('id', id).select('*').first() 427 | 428 | if (!res) return null 429 | if (res.photoURL) res.photoURL = file.getProxyPrefix() + res.photoURL 430 | 431 | return Object.assign({}, res) 432 | }) 433 | 434 | return result 435 | } 436 | 437 | /** 438 | * Attach meta to meatdata table 439 | * 440 | * @param {String} type 441 | * @param {Int} metadataId 442 | * @param {String} name 443 | * @param {String=} photoURL photo URL 444 | * 445 | * @return {Int} 446 | */ 447 | async attachMeta (type, metadataId, name, photoURL) { 448 | const map = this._getTypeMapping(type) 449 | logger.debug(map) 450 | 451 | try { 452 | const id = await this.getMetaId(map.type, name, photoURL) 453 | logger.debug(`${map.log} id`, id) 454 | 455 | const count = await db(`${map.type}_mapping`).where(map.column, id).where('metadataId', metadataId).count() 456 | if (count[0]['count(*)'] === 0) { 457 | logger.debug('Create mapping, count', count, count[0]['count(*)']) 458 | 459 | const data = { 460 | metadataId, 461 | updateTime: (new Date()).getTime() 462 | } 463 | data[map.column] = id 464 | 465 | await db(`${map.type}_mapping`).insert(data) 466 | } else { 467 | logger.debug('Meta exists') 468 | } 469 | } catch (error) { 470 | logger.error('Error while attaching a meta', error) 471 | throw error 472 | } 473 | } 474 | 475 | async searchMetadata (searchStr, page, size) { 476 | const params = `${searchStr}`.split(' ', parseInt(config.get('system.searchParmaNum')) || 3) 477 | 478 | let result = db('metadatas').orderBy('releaseDate', 'desc') 479 | 480 | const _param = params.shift() 481 | 482 | result = result.where((builder) => { 483 | builder.where('title', 'like', `%${_param}%`) 484 | .orWhere('companyName', 'like', `%${_param}%`) 485 | .orWhere('companyId', 'like', `%${_param}%`) 486 | }) 487 | 488 | for (const i in params) { 489 | const param = params[i] 490 | 491 | result = result.where((builder) => { 492 | builder.where('title', 'like', `%${param}%`) 493 | .orWhere('companyName', 'like', `%${param}%`) 494 | .orWhere('companyId', 'like', `%${param}%`) 495 | }) 496 | } 497 | 498 | let total = await result.clone().count() 499 | total = total[0]['count(*)'] 500 | 501 | result = await result.select('*').paginate({ 502 | perPage: size, 503 | currentPage: page 504 | }) 505 | 506 | result = result.data 507 | if (!result) return [] 508 | 509 | result = await this._processMetadataList(result) 510 | 511 | return { 512 | total, 513 | data: result 514 | } 515 | } 516 | 517 | /** 518 | * Get type mapping 519 | * 520 | * @param {String} type type(tag, series, star) 521 | * 522 | * @returns {Object} 523 | */ 524 | _getTypeMapping (type) { 525 | const map = {} 526 | switch (type) { 527 | case 'tag': 528 | map.log = 'Tag' 529 | map.column = 'tagId' 530 | map.type = 'tags' 531 | break 532 | case 'star': 533 | map.log = 'Star' 534 | map.column = 'starId' 535 | map.type = 'stars' 536 | break 537 | case 'series': 538 | map.log = 'Series' 539 | map.column = 'seriesId' 540 | map.type = 'series' 541 | } 542 | 543 | return map 544 | } 545 | 546 | /** 547 | * Process Knex Object to Object-Array 548 | * 549 | * @param {Object} result 550 | * @returns {Array} 551 | */ 552 | async _processMetadataList (result) { 553 | const processed = [] 554 | for (const i in result) { 555 | let item = result[i] 556 | item = Object.assign({}, item) 557 | 558 | item.JAVID = (`${item.JAVID}`.indexOf('-') !== -1) ? item.JAVID : (item.companyName + '-' + item.companyId) 559 | item.posterFileURL = file.getProxyPrefix() + item.posterFileURL 560 | 561 | if (item.version === 2) { 562 | item.screenshotFilesURL = JSON.parse(item.screenshotFilesURL) 563 | for (const i in item.screenshotFilesURL) { 564 | item.screenshotFilesURL[i] = file.getProxyPrefix() + item.screenshotFilesURL[i] 565 | } 566 | } else item.screenshotFilesURL = [] 567 | 568 | processed.push(Object.assign(item, await this.getMetaByMetadataId(item.id))) 569 | } 570 | 571 | return processed 572 | } 573 | } 574 | 575 | module.exports = new Metadata() 576 | --------------------------------------------------------------------------------