Skip to content

Commit e018a5a

Browse files
authored
Merge pull request #97 from solidSpoon/dlvideo-new
v5.0.2
2 parents d6cd92a + 87dba9d commit e018a5a

File tree

14 files changed

+408
-317
lines changed

14 files changed

+408
-317
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "dash-player",
33
"productName": "DashPlayer",
4-
"version": "5.0.1",
4+
"version": "5.0.2",
55
"description": "My Electron application description",
66
"main": ".vite/build/main.js",
77
"scripts": {

src/backend/ioc/inversify.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ import DlVideoServiceImpl from '@/backend/services/impl/DlVideoServiceImpl';
6060
import WatchHistoryService from '@/backend/services/WatchHistoryService';
6161
import WatchHistoryServiceImpl from '@/backend/services/impl/WatchHistoryServiceImpl';
6262
import WatchHistoryController from '@/backend/controllers/WatchHistoryController';
63+
import { OpenAIServiceImpl } from '@/backend/services/impl/OpenAIServiceImpl';
64+
import { OpenAiService } from '@/backend/services/OpenAiService';
6365

6466

6567
const container = new Container();
@@ -102,4 +104,5 @@ container.bind<SplitVideoService>(TYPES.SplitVideoService).to(SplitVideoServiceI
102104
container.bind<MediaService>(TYPES.MediaService).to(MediaServiceImpl).inSingletonScope();
103105
container.bind<TranslateService>(TYPES.TranslateService).to(TranslateServiceImpl).inSingletonScope();
104106
container.bind<WatchHistoryService>(TYPES.WatchHistoryService).to(WatchHistoryServiceImpl).inSingletonScope();
107+
container.bind<OpenAiService>(TYPES.OpenAiService).to(OpenAIServiceImpl).inSingletonScope();
105108
export default container;

src/backend/ioc/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const TYPES = {
2121
MediaService: Symbol('MediaService'),
2222
TranslateService: Symbol('TranslateService'),
2323
WatchHistoryService: Symbol('WatchHistoryService'),
24+
OpenAiService: Symbol('OpenAiService'),
2425
// Clients
2526
YouDaoClientProvider: Symbol('YouDaoClientProvider'),
2627
TencentClientProvider: Symbol('TencentClientProvider'),

src/backend/objs/OpenAiWhisperRequest.ts

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
import { storeGet } from '@/backend/store';
22
import fs from 'fs';
33
import RateLimiter from '@/common/utils/RateLimiter';
4-
import FormData from 'form-data';
5-
import axios, { CancelTokenSource } from 'axios';
6-
import UrlUtil from '@/common/utils/UrlUtil';
7-
import dpLog from '@/backend/ioc/logger';
84
import StrUtil from '@/common/utils/str-util';
95
import { Cancelable } from '@/common/interfaces';
10-
import CancelByUserError from '@/backend/errors/CancelByUserError';
6+
import OpenAI from 'openai';
7+
8+
import { z } from 'zod';
9+
import dpLog from '@/backend/ioc/logger';
10+
11+
const WhisperResponseVerifySchema = z.object({
12+
language: z.string(),
13+
duration: z.union([z.number(), z.string()]),
14+
text: z.string(),
15+
segments: z.array(z.object({
16+
seek: z.number(),
17+
start: z.number(),
18+
end: z.number(),
19+
text: z.string()
20+
}))
21+
});
1122

1223
export interface WhisperResponse {
1324
language: string;
@@ -23,73 +34,60 @@ export interface WhisperResponse {
2334
}
2435

2536
class OpenAiWhisperRequest implements Cancelable {
26-
private readonly apiKey: string;
27-
private readonly endpoint: string;
2837
private readonly file: string;
29-
private cancelTokenSource: CancelTokenSource | null = null;
38+
private abortController: AbortController | null = null;
39+
public readonly openAi: OpenAI;
3040

31-
constructor(file: string, apiKey: string, endpoint: string) {
41+
constructor(openai: OpenAI, file: string) {
3242
this.file = file;
33-
this.apiKey = apiKey;
34-
this.endpoint = endpoint;
43+
this.openAi = openai;
3544
}
3645

37-
public static build(file: string): OpenAiWhisperRequest | null {
46+
public static build(openai: OpenAI, file: string): OpenAiWhisperRequest | null {
3847
const apiKey = storeGet('apiKeys.openAi.key');
3948
const endpoint = storeGet('apiKeys.openAi.endpoint');
4049
if (StrUtil.hasBlank(file, apiKey, endpoint)) {
4150
return null;
4251
}
43-
return new OpenAiWhisperRequest(file, apiKey, endpoint);
52+
return new OpenAiWhisperRequest(openai, file);
4453
}
4554

4655
public async invoke(): Promise<WhisperResponse> {
47-
if (this.cancelTokenSource) {
48-
this.cancelTokenSource.cancel('Operation canceled by the user');
49-
this.cancelTokenSource = null;
50-
}
56+
this.cancel();
5157
await RateLimiter.wait('whisper');
52-
const data = new FormData();
53-
data.append('file', fs.createReadStream(this.file) as any);
54-
data.append('model', 'whisper-1');
55-
data.append('language', 'en');
56-
data.append('response_format', 'verbose_json');
57-
58-
this.cancelTokenSource = axios.CancelToken.source();
59-
60-
// 创建一个 CancelToken 的实例
61-
const config = {
62-
method: 'post',
63-
url: UrlUtil.joinWebUrl(this.endpoint, '/v1/audio/transcriptions'),
64-
headers: {
65-
'Accept': 'application/json',
66-
'Authorization': `Bearer ${this.apiKey}`,
67-
'Content-Type': 'multipart/form-data',
68-
...data.getHeaders()
69-
},
70-
data: data,
71-
timeout: 1000 * 60 * 10,
72-
cancelToken: this.cancelTokenSource.token
73-
};
74-
75-
const response = await axios(config)
76-
.catch((error) => {
77-
if (axios.isCancel(error)) {
78-
dpLog.info('Request canceled', error.message);
79-
throw new CancelByUserError();
80-
}
81-
dpLog.error('Request error', error);
82-
throw error;
83-
});
58+
this.abortController = new AbortController();
59+
const transcription = await this.openAi.audio.transcriptions.create({
60+
file: fs.createReadStream(this.file),
61+
model: "whisper-1",
62+
response_format: "verbose_json",
63+
timestamp_granularities: ["segment"]
64+
}, {signal: this.abortController.signal});
65+
// 用 zed 校验一下 transcription 是否为 类型 TranscriptionVerbose
66+
const parseRes = WhisperResponseVerifySchema.safeParse(transcription);
67+
if (!parseRes.success) {
68+
// dperror 为什么不匹配
69+
dpLog.error('Invalid response from OpenAI', parseRes.error.errors);
70+
throw new Error('Invalid response from OpenAI');
71+
}
8472
return {
85-
...response.data
86-
};
73+
language: transcription.language,
74+
duration: Number(transcription.duration),
75+
text: transcription.text,
76+
offset: 0,
77+
segments: transcription.segments?.map((seg) => ({
78+
seek: seg.seek,
79+
start: seg.start,
80+
end: seg.end,
81+
text: seg.text
82+
}))??[]
83+
}
84+
8785
}
8886

8987
public cancel(): void {
90-
if (this.cancelTokenSource) {
91-
this.cancelTokenSource.cancel('Operation canceled by the user');
92-
this.cancelTokenSource = null;
88+
if (this.abortController) {
89+
this.abortController.abort('Operation canceled by the user');
90+
this.abortController = null;
9391
}
9492
}
9593

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import LocationService, { ProgramType } from '@/backend/services/LocationService';
2+
import { cookieType, DlVideoContext } from '@/common/types/DlVideoType';
3+
import { spawn } from 'child_process';
4+
import ChildProcessTask from '@/backend/objs/ChildProcessTask';
5+
import dpLog from '@/backend/ioc/logger';
6+
import container from '@/backend/ioc/inversify.config';
7+
import TYPES from '@/backend/ioc/types';
8+
import { Cancelable } from '@/common/interfaces';
9+
10+
export class DlpDownloadVideo implements Cancelable {
11+
private onLog: (progress:number, msg: string) => void = () => {
12+
return;
13+
};
14+
15+
public setOnLog(callback: (progress:number, msg: string) => void) {
16+
this.onLog = callback;
17+
}
18+
19+
private log(msg: string) {
20+
dpLog.info(msg);
21+
this.onLog(this.progress, msg);
22+
}
23+
24+
readonly context: DlVideoContext;
25+
readonly ytDlpPath: string;
26+
readonly ffmpegPath: string;
27+
private task: ChildProcessTask | null = null;
28+
private progress = 0;
29+
30+
constructor(context: DlVideoContext) {
31+
const locationService = container.get<LocationService>(TYPES.LocationService);
32+
this.ytDlpPath = locationService.getThirdLibPath(ProgramType.YT_DL);
33+
this.ffmpegPath = locationService.getThirdLibPath(ProgramType.LIB);
34+
this.context = context;
35+
}
36+
37+
public cancel(): void {
38+
if (this.task) {
39+
this.task.cancel();
40+
}
41+
}
42+
43+
44+
public async run(): Promise<void> {
45+
this.log('System: downloading video');
46+
47+
return new Promise<void>((resolve, reject) => {
48+
49+
const cookiesArg = this.context.cookies === cookieType('no-cookie') ? [] : ['--cookies-from-browser', this.context.cookies];
50+
51+
const args = [
52+
'--ffmpeg-location',
53+
this.ffmpegPath,
54+
'-f',
55+
'bestvideo[height<=1080][height>=?720]+bestaudio/best',
56+
...cookiesArg,
57+
'-o',
58+
'%(title)s.%(ext)s',
59+
'--merge-output-format',
60+
'mp4',
61+
'--remux-video',
62+
'mp4',
63+
'-P',
64+
this.context.savePath,
65+
this.context.url
66+
];
67+
68+
const ytDlpProcess = spawn(this.ytDlpPath, args);
69+
70+
ytDlpProcess.stdout.setEncoding('utf8');
71+
ytDlpProcess.stderr.setEncoding('utf8');
72+
let percentProgress = 0;
73+
74+
this.task = new ChildProcessTask(ytDlpProcess);
75+
76+
ytDlpProcess.stdout.on('data', (data: string) => {
77+
const progressMatch = data.match(/\[download\]\s+(\d+(\.\d+)?)%/);
78+
if (progressMatch) {
79+
percentProgress = parseFloat(progressMatch[1]);
80+
console.log(`Download progress: ${percentProgress}%`);
81+
this.progress = percentProgress;
82+
}
83+
this.log(data);
84+
});
85+
86+
ytDlpProcess.stderr.on('data', (data: string) => {
87+
this.log(data);
88+
});
89+
90+
ytDlpProcess.on('close', (code: number) => {
91+
console.log(`yt-dlp process exited with code ${code}`);
92+
if (code === 0) {
93+
resolve();
94+
} else {
95+
const errorMsg = `yt-dlp process exited with code ${code}`;
96+
this.log(errorMsg);
97+
reject(new Error(errorMsg));
98+
}
99+
});
100+
});
101+
}
102+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import LocationService, { ProgramType } from '@/backend/services/LocationService';
2+
import { cookieType, DlVideoContext } from '@/common/types/DlVideoType';
3+
import { spawn } from 'child_process';
4+
import ChildProcessTask from '@/backend/objs/ChildProcessTask';
5+
import dpLog from '@/backend/ioc/logger';
6+
import container from '@/backend/ioc/inversify.config';
7+
import TYPES from '@/backend/ioc/types';
8+
import { Cancelable } from '@/common/interfaces';
9+
10+
export class DlpFetchFileName implements Cancelable {
11+
private onLog: (msg: string) => void = () => {
12+
return;
13+
};
14+
15+
public setOnLog(callback: (msg: string) => void) {
16+
this.onLog = callback;
17+
}
18+
19+
private log(msg: string) {
20+
dpLog.info(msg);
21+
this.onLog(msg);
22+
}
23+
24+
readonly ytDlpPath: string;
25+
readonly ffmpegPath: string;
26+
private task: ChildProcessTask | null = null;
27+
readonly context: DlVideoContext;
28+
29+
constructor(context: DlVideoContext) {
30+
const locationService = container.get<LocationService>(TYPES.LocationService);
31+
this.ytDlpPath = locationService.getThirdLibPath(ProgramType.YT_DL);
32+
this.ffmpegPath = locationService.getThirdLibPath(ProgramType.LIB);
33+
this.context = context;
34+
}
35+
36+
public cancel(): void {
37+
if (this.task) {
38+
this.task.cancel();
39+
}
40+
}
41+
42+
43+
public async run(): Promise<string> {
44+
this.log('System: fetching video file name');
45+
46+
return new Promise<string>((resolve, reject) => {
47+
let output = '';
48+
49+
const cookiesArg = this.context.cookies === cookieType('no-cookie') ? [] : ['--cookies-from-browser', this.context.cookies];
50+
51+
const ytDlpProcess = spawn(this.ytDlpPath, [
52+
'--ffmpeg-location',
53+
this.ffmpegPath,
54+
...cookiesArg,
55+
'--get-filename',
56+
'-o',
57+
'%(title)s.%(ext)s',
58+
this.context.url
59+
]);
60+
61+
ytDlpProcess.stdout.setEncoding('utf8');
62+
63+
this.task = new ChildProcessTask(ytDlpProcess);
64+
65+
ytDlpProcess.stdout.on('data', (data: string) => {
66+
output += data.toString();
67+
this.log(data.toString());
68+
});
69+
70+
ytDlpProcess.stderr.on('data', (data: string) => {
71+
console.error('Error:', data.toString());
72+
this.log(data.toString());
73+
});
74+
75+
ytDlpProcess.on('close', (code: number) => {
76+
if (code === 0 && output.trim()) {
77+
resolve(output.trim());
78+
} else {
79+
const errorMsg = `yt-dlp process exited with code ${code}`;
80+
this.log(errorMsg);
81+
reject(new Error(errorMsg));
82+
}
83+
});
84+
});
85+
}
86+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import OpenAI from 'openai';
2+
3+
export interface OpenAiService {
4+
getOpenAi(): OpenAI;
5+
}

0 commit comments

Comments
 (0)