diff --git a/packages/engine-media/src/index.ts b/packages/engine-media/src/index.ts new file mode 100644 index 0000000..af7e4b7 --- /dev/null +++ b/packages/engine-media/src/index.ts @@ -0,0 +1,101 @@ +import type { ChronocatContext } from '@chronocat/shell' +import AdmZip from 'adm-zip' +import { execFile } from 'node:child_process' +import { BinaryLike, createHash } from 'node:crypto' +import { mkdir, unlink, writeFile } from 'node:fs/promises' +import { homedir } from 'node:os' +import { join } from 'node:path' +import { arch, platform } from 'node:process' +import ntsilkZip from '../../../build/caches/ntsilk.zip' + +declare const __DEFINE_CHRONO_VERSION__: string + +export const name = 'engine-media' +export const version = __DEFINE_CHRONO_VERSION__ + +const durationRegex = /^ *Duration: (\d+):(\d+):(\d+).(\d+),/m + +export const apply = async (ctx: ChronocatContext) => { + let ntsilkPath: string + + try { + ntsilkPath = await load(ctx) + } catch (cause) { + ctx.chronocat.l.warn( + new Error('engine-crychiccat: 加载失败', { + cause, + }), + { + code: 2162, + }, + ) + + return + } + + const register = ctx.chronocat.api.register(name) + + register( + 'chronocat.internal.media.ntsilk.encode', + (payload) => + new Promise((res, rej) => { + execFile( + ntsilkPath, + ['-y', '-i', payload.srcPath, '-f', 'ntsilk_s16le', payload.dstPath], + {}, + (err, _stdout, stderr) => { + if (err) { + // 清理可能存在的已转码文件 + ctx.chronocat.exists(payload.dstPath).then((x) => { + if (x) void unlink(payload.dstPath) + }) + + rej(err) + return + } + + const capture = durationRegex.exec(stderr) + const hrs = Number(capture?.[1]) || 0 + const min = Number(capture?.[2]) || 0 + const sec = Number(capture?.[3]) || 0 + const ms = Number(capture?.[4]) || 0 + + res({ + duration: 360000 * hrs + 6000 * min + 100 * sec + ms, + waveAmplitudes: undefined, + }) + }, + ) + }), + ) +} + +const suffix = platform === 'win32' ? '.exe' : '' +const entryName = `ntsilk-${platform}-${arch}${suffix}` + +const load = async (ctx: ChronocatContext) => { + const data = await new Promise((res, rej) => { + new AdmZip(Buffer.from(ntsilkZip)).readFileAsync(entryName, (data, err) => + err ? rej(new Error(`读取文件失败`)) : res(data!), + ) + }) + + const hash = getHash(data) + + const dir = join(homedir(), `.chronocat/tmp/native`) + + await mkdir(dir, { + recursive: true, + }) + + const path = join(dir, `${hash}${suffix}`) + if (!(await ctx.chronocat.exists(path))) await writeFile(path, data) + + return path +} + +const getHash = (data: BinaryLike) => { + const hash = createHash('sha256') + hash.update(data) + return hash.digest('hex') +} diff --git a/packages/engine-media/src/types.d.ts b/packages/engine-media/src/types.d.ts new file mode 100644 index 0000000..908d264 --- /dev/null +++ b/packages/engine-media/src/types.d.ts @@ -0,0 +1,6 @@ +declare const zipData: Uint8Array + +declare module '*.zip' { + // eslint-disable-next-line import/no-default-export + export default zipData +} diff --git a/packages/shell/src/types.ts b/packages/shell/src/types.ts index ef8c76d..afbe86f 100644 --- a/packages/shell/src/types.ts +++ b/packages/shell/src/types.ts @@ -101,6 +101,16 @@ export interface SelfProfileDispatchMessage { }> } +export interface InternalMediaNtsilkEncodePayload { + srcPath: string + dstPath: string +} + +export interface InternalMediaNtsilkEncodeResponse { + duration: number | undefined + waveAmplitudes: number[] | undefined +} + export interface SatoriMethods { 'channel.get': [[ChannelGetPayload], Channel] 'channel.list': [[ChannelListPayload], ChannelListResponse] @@ -197,6 +207,11 @@ export interface CCInternalMethods { 'chronocat.internal.uix.uin.get': [[string], string | undefined] 'chronocat.internal.uix.uin.get.group': [[string, string], string | undefined] + + 'chronocat.internal.media.ntsilk.encode': [ + [InternalMediaNtsilkEncodePayload], + InternalMediaNtsilkEncodeResponse, + ] } export type Methods = SatoriMethods & CCInternalMethods