Commit 1006a158 authored by nanahira's avatar nanahira

rework

parent 1e98a3df
Pipeline #5728 passed with stages
in 33 minutes and 18 seconds
FROM node:buster-slim FROM node:bullseye-slim
RUN apt update && apt -y install python3 build-essential && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN apt update && apt -y install python3 build-essential && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY ./package*.json ./ COPY ./package*.json ./
......
This diff is collapsed.
...@@ -21,25 +21,24 @@ ...@@ -21,25 +21,24 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@nestjs/axios": "^0.0.1",
"@nestjs/common": "^7.5.1", "@nestjs/common": "^7.5.1",
"@nestjs/config": "^1.0.1",
"@nestjs/core": "^7.5.1", "@nestjs/core": "^7.5.1",
"@nestjs/platform-express": "^7.5.1", "@nestjs/platform-express": "^7.5.1",
"@nestjs/swagger": "^4.8.0", "@nestjs/swagger": "^4.8.0",
"@nestjs/typeorm": "^7.1.5", "@nestjs/typeorm": "^7.1.5",
"@types/cookie": "^0.4.0", "@types/cookie": "^0.4.0",
"axios": "^0.21.1",
"class-transformer": "^0.4.0", "class-transformer": "^0.4.0",
"class-validator": "^0.13.1", "class-validator": "^0.13.1",
"cookie": "^0.4.1", "cookie": "^0.4.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.1", "moment": "^2.29.1",
"mysql": "^2.18.1",
"p-queue": "^6.6.2", "p-queue": "^6.6.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"swagger-ui-express": "^4.1.6", "swagger-ui-express": "^4.1.6"
"typeorm": "^0.2.32"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^7.5.1", "@nestjs/cli": "^7.5.1",
......
import { Controller, Get } from '@nestjs/common'; import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
@Controller() @Controller()
@ApiTags('tips') @ApiTags('tips')
...@@ -12,6 +12,7 @@ export class AppController { ...@@ -12,6 +12,7 @@ export class AppController {
summary: '获取提示', summary: '获取提示',
description: '获取 SRVPro 需要的 tips.json 文件。', description: '获取 SRVPro 需要的 tips.json 文件。',
}) })
@ApiOkResponse({ type: [String] })
async getTips(): Promise<string[]> { async getTips(): Promise<string[]> {
return await this.appService.getTips(); return await this.appService.getTips();
} }
......
import { Injectable, Scope, Logger } from '@nestjs/common';
@Injectable()
export class AppLogger extends Logger {}
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { AppLogger } from './app.logger'; import { HttpModule } from '@nestjs/axios';
import { ConfigModule } from '@nestjs/config';
@Module({ @Module({
imports: [], imports: [HttpModule, ConfigModule.forRoot()],
controllers: [AppController], controllers: [AppController],
providers: [AppService, AppLogger], providers: [AppService],
}) })
export class AppModule {} export class AppModule {}
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { AppLogger } from './app.logger';
import moment from 'moment'; import moment from 'moment';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import axios from 'axios';
import _ from 'lodash'; import _ from 'lodash';
import qs from 'qs'; import qs from 'qs';
import cookie from 'cookie'; import cookie from 'cookie';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
const blacklistedWords = ['', '', '', '', '[Vv]ocal', ':', '']; const blacklistedWords = ['', '', '', '', '[Vv]ocal', ':', ''];
const blacklistedRegex = blacklistedWords.map((word) => new RegExp(word)); const blacklistedRegex = blacklistedWords.map((word) => new RegExp(word));
...@@ -16,55 +16,43 @@ interface LoginInfo { ...@@ -16,55 +16,43 @@ interface LoginInfo {
} }
@Injectable() @Injectable()
export class AppService { export class AppService extends Logger {
cache: string[]; cache: string[];
lastFetchTime: moment.Moment; lastFetchTime: moment.Moment;
queue: PQueue; queue = new PQueue({ concurrency: 1 });
loginInfo: LoginInfo; loginInfo: LoginInfo;
constructor(private log: AppLogger) { constructor(config: ConfigService, private http: HttpService) {
this.log.setContext('ygopro-tips-biu-generator'); super('ygopro-tips-biu-generator');
this.queue = new PQueue({ concurrency: 1 }); if (config.get<string>('BIU_EMAIL') && config.get<string>('BIU_PASSWORD')) {
if (process.env.BIU_EMAIL && process.env.BIU_PASSWORD) {
this.loginInfo = { this.loginInfo = {
email: process.env.BIU_EMAIL, email: config.get<string>('BIU_EMAIL'),
password: process.env.BIU_PASSWORD, password: config.get<string>('BIU_PASSWORD'),
}; };
} }
} }
async getTips(): Promise<string[]> { async getTips(): Promise<string[]> {
if ( return this.fetchTips();
this.cache &&
this.lastFetchTime &&
moment().diff(this.lastFetchTime) <= 3600000
) {
return this.cache;
}
this.log.log(`Queue size: ${this.queue.size}`);
if (this.queue.size > 0) {
await this.queue.onEmpty();
return this.cache;
} else {
return await this.fetchTips();
}
} }
async fetchTips(): Promise<string[]> { async fetchTips(): Promise<string[]> {
return await this.queue.add(async () => await this.fetchTipsTask()); return await this.queue.add(async () => this.fetchTipsTask());
} }
async getLyricsOfSong(id: number): Promise<string[]> { async getLyricsOfSong(id: number): Promise<string[]> {
try { try {
this.log.log(`Fetching lyrics of song ${id}.`); this.log(`Fetching lyrics of song ${id}.`);
const { data } = await axios.get(`https://web.biu.moe/s${id}`, { const { data } = await this.http
responseType: 'document', .get(`https://web.biu.moe/s${id}`, {
timeout: 30000, responseType: 'document',
}); timeout: 30000,
})
.toPromise();
const lyricMatches = (data as string).match( const lyricMatches = (data as string).match(
/\[\d+:\d+[:\.]\d+\]([^<]+)/g, /\[\d+:\d+[:\.]\d+\]([^<]+)/g,
); );
if (!lyricMatches) { if (!lyricMatches) {
this.log.log(`Song ${id} has no lyrics.`); this.log(`Song ${id} has no lyrics.`);
return []; return [];
} }
const lyrics = lyricMatches const lyrics = lyricMatches
...@@ -74,10 +62,10 @@ export class AppService { ...@@ -74,10 +62,10 @@ export class AppService {
.filter((lyric) => .filter((lyric) =>
blacklistedRegex.every((regex) => !lyric.match(regex)), blacklistedRegex.every((regex) => !lyric.match(regex)),
); );
this.log.log(`${lyrics.length} lines of lyrics found in song ${id}.`); this.log(`${lyrics.length} lines of lyrics found in song ${id}.`);
return lyrics; return lyrics;
} catch (e) { } catch (e) {
this.log.error(`Failed fetching lyrics of ${id}: ${e.toString()}`); this.error(`Failed fetching lyrics of ${id}: ${e.toString()}`);
return []; return [];
} }
} }
...@@ -87,17 +75,19 @@ export class AppService { ...@@ -87,17 +75,19 @@ export class AppService {
return undefined; return undefined;
} }
try { try {
this.log.log(`Logging in.`); this.log(`Logging in.`);
const ret = await axios.post( const ret = await this.http
'https://web.biu.moe/User/doLogin', .post(
qs.stringify(this.loginInfo), 'https://web.biu.moe/User/doLogin',
{ qs.stringify(this.loginInfo),
responseType: 'json', {
timeout: 30000, responseType: 'json',
}, timeout: 30000,
); },
)
.toPromise();
if (!ret.data.status) { if (!ret.data.status) {
this.log.error(`Login failed: ${ret.data.message}`); this.error(`Login failed: ${ret.data.message}`);
return undefined; return undefined;
} }
const setCookieContent = ret.headers['set-cookie'] as string[]; const setCookieContent = ret.headers['set-cookie'] as string[];
...@@ -109,45 +99,54 @@ export class AppService { ...@@ -109,45 +99,54 @@ export class AppService {
return undefined; return undefined;
} }
const token = cookie.parse(contentHits).biuInfo as string; const token = cookie.parse(contentHits).biuInfo as string;
this.log.log(`Login success: ${token}`); this.log(`Login success: ${token}`);
return cookie.serialize('biuInfo', token); return cookie.serialize('biuInfo', token);
} catch (e) { } catch (e) {
this.log.error(`Login error: ${e.toString()}`); this.error(`Login error: ${e.toString()}`);
return undefined; return undefined;
} }
} }
async fetchTipsTask(): Promise<string[]> { async fetchTipsTask(): Promise<string[]> {
if (
this.cache &&
this.lastFetchTime &&
moment().diff(this.lastFetchTime) <= 3600000
) {
return this.cache;
}
try { try {
this.log.log(`Fetching song list.`); this.log(`Fetching song list.`);
const { data } = await axios.get('https://web.biu.moe/Index/home', { const { data } = await this.http
responseType: 'document', .get('https://web.biu.moe/Index/home', {
headers: { responseType: 'document',
Cookie: (await this.getLoginCookie()) || '', headers: {
}, Cookie: (await this.getLoginCookie()) || '',
timeout: 30000, },
}); timeout: 30000,
})
.toPromise();
const urls = (data as string).match(/href="\/s(\d+)"/g); const urls = (data as string).match(/href="\/s(\d+)"/g);
if (!urls) { if (!urls) {
this.log.error(`No songs found, retrying.`); this.error(`No songs found, retrying.`);
return await this.fetchTipsTask(); return await this.fetchTipsTask();
} }
this.log.log(`${urls.length} songs found.`); this.log(`${urls.length} songs found.`);
const songIds = urls.map((m) => parseInt(m.match(/href="\/s(\d+)"/)[1])); const songIds = urls.map((m) => parseInt(m.match(/href="\/s(\d+)"/)[1]));
const lyrics = _.flatten( const lyrics = _.flatten(
await Promise.all(songIds.map((id) => this.getLyricsOfSong(id))), await Promise.all(songIds.map((id) => this.getLyricsOfSong(id))),
); );
this.log.log(`${lyrics.length} lines of lyrics got.`); this.log(`${lyrics.length} lines of lyrics got.`);
if (lyrics.length) { if (lyrics.length) {
this.cache = lyrics; this.cache = lyrics;
this.lastFetchTime = moment(); this.lastFetchTime = moment();
return lyrics; return lyrics;
} else { } else {
this.log.log(`No lyrics found, retrying.`); this.log(`No lyrics found, retrying.`);
return await this.fetchTipsTask(); return await this.fetchTipsTask();
} }
} catch (e) { } catch (e) {
this.log.error(`Errored fetching song list, retrying: ${e.toString()}`); this.error(`Errored fetching song list, retrying: ${e.toString()}`);
return await this.fetchTipsTask(); return await this.fetchTipsTask();
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment