mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 20:19:09 +08:00
feat: 支持微信视频消息解析与播放
支持视频 XML 解析、本地文件映射与拖动播放。 修复源码乱码注释并统一 UTF-8 编码。
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import crypto from 'crypto'
|
||||
import type { Wcdb4Client } from './wcdb4-client'
|
||||
|
||||
type VideoAsset = {
|
||||
filePath: string
|
||||
posterPath?: string
|
||||
}
|
||||
|
||||
export class VideoAssetService {
|
||||
private readonly urlTokens = new Map<string, string>()
|
||||
private index: Map<string, VideoAsset> | null = null
|
||||
|
||||
constructor(private readonly client: Wcdb4Client) {}
|
||||
|
||||
resolve(hashes: string[]): { success: boolean; url?: string; poster?: string; error?: string } {
|
||||
const candidates = Array.from(
|
||||
new Set(
|
||||
hashes
|
||||
.map((value) =>
|
||||
String(value || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
)
|
||||
.filter((value) => /^[a-f0-9]{32}$/.test(value))
|
||||
)
|
||||
)
|
||||
if (candidates.length === 0) return { success: false, error: '视频标识为空' }
|
||||
|
||||
const hardlinkDb = path.join(
|
||||
this.client.getAccountRoot(),
|
||||
'db_storage',
|
||||
'hardlink',
|
||||
'hardlink.db'
|
||||
)
|
||||
const lookupKeys = [...candidates]
|
||||
if (fs.existsSync(hardlinkDb)) {
|
||||
for (const hash of candidates) {
|
||||
const resolved = this.client.resolveVideoHardlink(hash, hardlinkDb)?.resolved_md5
|
||||
if (resolved) lookupKeys.unshift(String(resolved).trim().toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
const index = this.getIndex()
|
||||
for (const key of lookupKeys) {
|
||||
const asset = index.get(key) || index.get(`${key}_raw`)
|
||||
if (!asset) continue
|
||||
return {
|
||||
success: true,
|
||||
url: this.createUrl(asset.filePath),
|
||||
poster: asset.posterPath ? this.createUrl(asset.posterPath) : undefined
|
||||
}
|
||||
}
|
||||
return { success: false, error: '本地未找到该视频文件' }
|
||||
}
|
||||
|
||||
pathForToken(token: string): string | undefined {
|
||||
const filePath = this.urlTokens.get(token)
|
||||
if (!filePath || !fs.existsSync(filePath)) return undefined
|
||||
return filePath
|
||||
}
|
||||
|
||||
private createUrl(filePath: string): string {
|
||||
const token = crypto.randomBytes(18).toString('hex')
|
||||
this.urlTokens.set(token, filePath)
|
||||
if (this.urlTokens.size > 500) {
|
||||
const first = this.urlTokens.keys().next().value
|
||||
if (first) this.urlTokens.delete(first)
|
||||
}
|
||||
return `wxe-media://local/${token}`
|
||||
}
|
||||
|
||||
private getIndex(): Map<string, VideoAsset> {
|
||||
if (this.index) return this.index
|
||||
const result = new Map<string, VideoAsset>()
|
||||
const root = path.join(this.client.getAccountRoot(), 'msg', 'video')
|
||||
if (!fs.existsSync(root)) {
|
||||
this.index = result
|
||||
return result
|
||||
}
|
||||
|
||||
for (const month of fs.readdirSync(root)) {
|
||||
const monthPath = path.join(root, month)
|
||||
if (!fs.statSync(monthPath).isDirectory()) continue
|
||||
for (const name of fs.readdirSync(monthPath)) {
|
||||
const match = /^([a-f0-9]{32})(?:(_raw))?\.(mp4|jpg)$/i.exec(name)
|
||||
if (!match) continue
|
||||
const key = `${match[1].toLowerCase()}${match[2] || ''}`
|
||||
const fullPath = path.join(monthPath, name)
|
||||
const existing = result.get(key) || { filePath: '' }
|
||||
if (match[3].toLowerCase() === 'mp4') existing.filePath = fullPath
|
||||
else if (!existing.posterPath) existing.posterPath = fullPath
|
||||
result.set(key, existing)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, asset] of result) {
|
||||
if (!asset.filePath) result.delete(key)
|
||||
}
|
||||
this.index = result
|
||||
return result
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user