mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 11:37:06 +08:00
fix: 修复视频定位与运行时依赖打包
This commit is contained in:
@@ -22,6 +22,12 @@ const state = vi.hoisted(() => ({
|
||||
videoPath: '',
|
||||
messages: [] as Message[],
|
||||
messagesByUser: {} as Record<string, Message[]>,
|
||||
videoLookups: [] as {
|
||||
createTime?: number
|
||||
duration?: number
|
||||
width?: number
|
||||
height?: number
|
||||
}[],
|
||||
imageLookups: [] as {
|
||||
allowThumbnail?: boolean
|
||||
preferThumbnail?: boolean
|
||||
@@ -120,7 +126,11 @@ vi.mock('../../src/main/image-decrypt-service', () => ({
|
||||
}))
|
||||
vi.mock('../../src/main/video-asset-service', () => ({
|
||||
VideoAssetService: class {
|
||||
resolve(): { success: boolean; url: string } {
|
||||
resolve(
|
||||
_hashes: string[],
|
||||
options?: { createTime?: number; duration?: number; width?: number; height?: number }
|
||||
): { success: boolean; url: string } {
|
||||
state.videoLookups.push(options || {})
|
||||
return { success: true, url: 'wxe-media://local/fixture-video' }
|
||||
}
|
||||
pathForUrl(): string {
|
||||
@@ -175,6 +185,7 @@ describe('media export flow', () => {
|
||||
Buffer.from('000000186674797069736f6d0000020069736f6d69736f32', 'hex')
|
||||
)
|
||||
state.imageLookups = []
|
||||
state.videoLookups = []
|
||||
state.messagesByUser = {}
|
||||
const fileMonth = join(state.accountRoot, 'msg', 'file', '2026-08')
|
||||
mkdirSync(fileMonth, { recursive: true })
|
||||
@@ -203,7 +214,13 @@ describe('media export flow', () => {
|
||||
message({
|
||||
id: 'video',
|
||||
type: '视频',
|
||||
contentData: { type: 'video', md5: 'b'.repeat(32) }
|
||||
contentData: {
|
||||
type: 'video',
|
||||
md5: 'b'.repeat(32),
|
||||
duration: 68,
|
||||
width: 279,
|
||||
height: 630
|
||||
}
|
||||
}),
|
||||
message({
|
||||
id: 'file',
|
||||
@@ -264,6 +281,12 @@ describe('media export flow', () => {
|
||||
sessionMd5: 'fixture-user',
|
||||
createTime: 1_785_549_600
|
||||
})
|
||||
expect(state.videoLookups[0]).toEqual({
|
||||
createTime: 1_785_549_600,
|
||||
duration: 68,
|
||||
width: 279,
|
||||
height: 630
|
||||
})
|
||||
expect(progress.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
|
||||
@@ -5,12 +5,15 @@ import { dirname, join, resolve } from 'path'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url)
|
||||
const { validateFfmpegRuntime, validateSilkWasmRuntime } = nodeRequire(
|
||||
'../../scripts/after-pack.cjs'
|
||||
) as {
|
||||
validateFfmpegRuntime: (runtimeResources: string, platform?: NodeJS.Platform) => void
|
||||
validateSilkWasmRuntime: (runtimeResources: string) => void
|
||||
const asar = nodeRequire('@electron/asar') as {
|
||||
createPackage: (source: string, destination: string) => Promise<void>
|
||||
}
|
||||
const { validateAsarRuntimeDependencies, validateFfmpegRuntime, validateSilkWasmRuntime } =
|
||||
nodeRequire('../../scripts/after-pack.cjs') as {
|
||||
validateAsarRuntimeDependencies: (runtimeResources: string) => void
|
||||
validateFfmpegRuntime: (runtimeResources: string, platform?: NodeJS.Platform) => void
|
||||
validateSilkWasmRuntime: (runtimeResources: string) => void
|
||||
}
|
||||
const root = mkdtempSync(join(tmpdir(), 'wxe-runtime-package-'))
|
||||
|
||||
describe('production runtime packaging', () => {
|
||||
@@ -32,6 +35,19 @@ describe('production runtime packaging', () => {
|
||||
expect(config).toContain('node_modules/silk-wasm/**')
|
||||
})
|
||||
|
||||
it('rejects an app archive with missing runtime dependencies', async () => {
|
||||
const resources = join(root, 'asar-resources')
|
||||
const source = join(root, 'asar-source')
|
||||
mkdirSync(source, { recursive: true })
|
||||
writeFileSync(join(source, 'package.json'), '{}')
|
||||
mkdirSync(resources, { recursive: true })
|
||||
await asar.createPackage(source, join(resources, 'app.asar'))
|
||||
|
||||
expect(() => validateAsarRuntimeDependencies(resources)).toThrow(
|
||||
/Missing packaged runtime dependencies:.*@electron-toolkit\/utils/
|
||||
)
|
||||
})
|
||||
|
||||
it('requires and unpacks the bundled ffmpeg-static executable', () => {
|
||||
const resources = join(root, 'ffmpeg-resources')
|
||||
const ffmpegPath = join(
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { createHash } from 'crypto'
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { VideoAssetService } from '../../src/main/video-asset-service'
|
||||
|
||||
const temporaryDirectories: string[] = []
|
||||
|
||||
const box = (type: string, payload: Buffer): Buffer => {
|
||||
const header = Buffer.alloc(8)
|
||||
header.writeUInt32BE(header.length + payload.length, 0)
|
||||
header.write(type, 4, 4, 'ascii')
|
||||
return Buffer.concat([header, payload])
|
||||
}
|
||||
|
||||
const mp4Fixture = (durationSeconds: number, marker: string): Buffer => {
|
||||
const movieHeader = Buffer.alloc(20)
|
||||
movieHeader.writeUInt32BE(1000, 12)
|
||||
movieHeader.writeUInt32BE(Math.round(durationSeconds * 1000), 16)
|
||||
return Buffer.concat([
|
||||
box('ftyp', Buffer.from('isom0000', 'ascii')),
|
||||
box('moov', box('mvhd', movieHeader)),
|
||||
box('mdat', Buffer.from(marker, 'utf8'))
|
||||
])
|
||||
}
|
||||
|
||||
const jpegFixture = (width: number, height: number): Buffer =>
|
||||
Buffer.from([
|
||||
0xff,
|
||||
0xd8,
|
||||
0xff,
|
||||
0xc0,
|
||||
0x00,
|
||||
0x11,
|
||||
0x08,
|
||||
(height >> 8) & 0xff,
|
||||
height & 0xff,
|
||||
(width >> 8) & 0xff,
|
||||
width & 0xff,
|
||||
0x03,
|
||||
0x01,
|
||||
0x11,
|
||||
0x00,
|
||||
0x02,
|
||||
0x11,
|
||||
0x00,
|
||||
0x03,
|
||||
0x11,
|
||||
0x00,
|
||||
0xff,
|
||||
0xd9
|
||||
])
|
||||
|
||||
const createService = (): {
|
||||
accountRoot: string
|
||||
service: VideoAssetService
|
||||
} => {
|
||||
const accountRoot = mkdtempSync(join(tmpdir(), 'wxe-video-assets-'))
|
||||
temporaryDirectories.push(accountRoot)
|
||||
return {
|
||||
accountRoot,
|
||||
service: new VideoAssetService({
|
||||
getAccountRoot: () => accountRoot,
|
||||
resolveVideoHardlink: () => null
|
||||
} as never)
|
||||
}
|
||||
}
|
||||
|
||||
const monthTimestamp = (year: number, month: number): number =>
|
||||
Math.floor(new Date(year, month - 1, 15, 12).getTime() / 1000)
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of temporaryDirectories.splice(0)) {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe('VideoAssetService local fallback', () => {
|
||||
it('finds a video by its content MD5 when the hardlink mapping is absent', async () => {
|
||||
const { accountRoot, service } = createService()
|
||||
const month = join(accountRoot, 'msg', 'video', '2026-07')
|
||||
mkdirSync(month, { recursive: true })
|
||||
const content = mp4Fixture(22, 'content-md5-match')
|
||||
const filePath = join(month, `${'2'.repeat(32)}.mp4`)
|
||||
writeFileSync(filePath, content)
|
||||
const contentHash = createHash('md5').update(content).digest('hex')
|
||||
|
||||
const result = await service.resolve([contentHash], {
|
||||
createTime: monthTimestamp(2026, 7)
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(service.pathForUrl(result.url!)).toBe(filePath)
|
||||
})
|
||||
|
||||
it('finds a uniquely matching video by month, thumbnail size, and duration', async () => {
|
||||
const { accountRoot, service } = createService()
|
||||
const month = join(accountRoot, 'msg', 'video', '2025-11')
|
||||
mkdirSync(month, { recursive: true })
|
||||
const stem = '66cecd68e095d87175fb5ed138de3cef'
|
||||
const filePath = join(month, `${stem}.mp4`)
|
||||
const posterPath = join(month, `${stem}_thumb.jpg`)
|
||||
writeFileSync(filePath, mp4Fixture(68.441, 'metadata-match'))
|
||||
writeFileSync(posterPath, jpegFixture(279, 630))
|
||||
|
||||
const result = await service.resolve(
|
||||
['c92c54c8eae4471be9cc18396daf8015', '021e8a18a765ce14f4c54f40065db98e'],
|
||||
{
|
||||
createTime: monthTimestamp(2025, 11),
|
||||
duration: 68,
|
||||
width: 279,
|
||||
height: 630
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(service.pathForUrl(result.url!)).toBe(filePath)
|
||||
expect(service.pathForUrl(result.poster!)).toBe(posterPath)
|
||||
})
|
||||
|
||||
it('does not guess when multiple files match the same metadata', async () => {
|
||||
const { accountRoot, service } = createService()
|
||||
const month = join(accountRoot, 'msg', 'video', '2026-02')
|
||||
mkdirSync(month, { recursive: true })
|
||||
for (const stem of ['a'.repeat(32), 'b'.repeat(32)]) {
|
||||
writeFileSync(join(month, `${stem}.mp4`), mp4Fixture(12, stem))
|
||||
writeFileSync(join(month, `${stem}_thumb.jpg`), jpegFixture(224, 398))
|
||||
}
|
||||
|
||||
const result = await service.resolve(['c'.repeat(32)], {
|
||||
createTime: monthTimestamp(2026, 2),
|
||||
duration: 12,
|
||||
width: 224,
|
||||
height: 398
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: false, error: '本地未找到该视频文件' })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user