视频解密丰富日志 方便定位

This commit is contained in:
xuncha
2026-02-28 16:51:18 +08:00
parent c88aa2c9d8
commit d63c37cd78

View File

@@ -1,5 +1,6 @@
import { join } from 'path' import { join } from 'path'
import { existsSync, readdirSync, statSync, readFileSync } from 'fs' import { existsSync, readdirSync, statSync, readFileSync, appendFileSync, mkdirSync } from 'fs'
import { app } from 'electron'
import { ConfigService } from './config' import { ConfigService } from './config'
import Database from 'better-sqlite3' import Database from 'better-sqlite3'
import { wcdbService } from './wcdbService' import { wcdbService } from './wcdbService'
@@ -18,6 +19,16 @@ class VideoService {
this.configService = new ConfigService() this.configService = new ConfigService()
} }
private log(message: string, meta?: Record<string, unknown>): void {
try {
const timestamp = new Date().toISOString()
const metaStr = meta ? ` ${JSON.stringify(meta)}` : ''
const logDir = join(app.getPath('userData'), 'logs')
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true })
appendFileSync(join(logDir, 'wcdb.log'), `[${timestamp}] [VideoService] ${message}${metaStr}\n`, 'utf8')
} catch {}
}
/** /**
* 获取数据库根目录 * 获取数据库根目录
*/ */
@@ -69,7 +80,12 @@ class VideoService {
const wxid = this.getMyWxid() const wxid = this.getMyWxid()
const cleanedWxid = this.cleanWxid(wxid) const cleanedWxid = this.cleanWxid(wxid)
if (!wxid) return undefined this.log('queryVideoFileName 开始', { md5, wxid, cleanedWxid, cachePath, dbPath })
if (!wxid) {
this.log('queryVideoFileName: wxid 为空')
return undefined
}
// 方法1优先在 cachePath 下查找解密后的 hardlink.db // 方法1优先在 cachePath 下查找解密后的 hardlink.db
if (cachePath) { if (cachePath) {
@@ -84,6 +100,7 @@ class VideoService {
for (const p of cacheDbPaths) { for (const p of cacheDbPaths) {
if (existsSync(p)) { if (existsSync(p)) {
try { try {
this.log('尝试缓存 hardlink.db', { path: p })
const db = new Database(p, { readonly: true }) const db = new Database(p, { readonly: true })
const row = db.prepare(` const row = db.prepare(`
SELECT file_name, md5 FROM video_hardlink_info_v4 SELECT file_name, md5 FROM video_hardlink_info_v4
@@ -94,10 +111,12 @@ class VideoService {
if (row?.file_name) { if (row?.file_name) {
const realMd5 = row.file_name.replace(/\.[^.]+$/, '') const realMd5 = row.file_name.replace(/\.[^.]+$/, '')
this.log('缓存 hardlink.db 命中', { file_name: row.file_name, realMd5 })
return realMd5 return realMd5
} }
this.log('缓存 hardlink.db 未命中', { path: p })
} catch (e) { } catch (e) {
// 忽略错误 this.log('缓存 hardlink.db 查询失败', { path: p, error: String(e) })
} }
} }
} }
@@ -105,7 +124,6 @@ class VideoService {
// 方法2使用 wcdbService.execQuery 查询加密的 hardlink.db // 方法2使用 wcdbService.execQuery 查询加密的 hardlink.db
if (dbPath) { if (dbPath) {
// 检查 dbPath 是否已经包含 wxid
const dbPathLower = dbPath.toLowerCase() const dbPathLower = dbPath.toLowerCase()
const wxidLower = wxid.toLowerCase() const wxidLower = wxid.toLowerCase()
const cleanedWxidLower = cleanedWxid.toLowerCase() const cleanedWxidLower = cleanedWxid.toLowerCase()
@@ -113,10 +131,8 @@ class VideoService {
const encryptedDbPaths: string[] = [] const encryptedDbPaths: string[] = []
if (dbPathContainsWxid) { if (dbPathContainsWxid) {
// dbPath 已包含 wxid不需要再拼接
encryptedDbPaths.push(join(dbPath, 'db_storage', 'hardlink', 'hardlink.db')) encryptedDbPaths.push(join(dbPath, 'db_storage', 'hardlink', 'hardlink.db'))
} else { } else {
// dbPath 不包含 wxid需要拼接
encryptedDbPaths.push(join(dbPath, wxid, 'db_storage', 'hardlink', 'hardlink.db')) encryptedDbPaths.push(join(dbPath, wxid, 'db_storage', 'hardlink', 'hardlink.db'))
encryptedDbPaths.push(join(dbPath, cleanedWxid, 'db_storage', 'hardlink', 'hardlink.db')) encryptedDbPaths.push(join(dbPath, cleanedWxid, 'db_storage', 'hardlink', 'hardlink.db'))
} }
@@ -124,27 +140,29 @@ class VideoService {
for (const p of encryptedDbPaths) { for (const p of encryptedDbPaths) {
if (existsSync(p)) { if (existsSync(p)) {
try { try {
this.log('尝试加密 hardlink.db', { path: p })
const escapedMd5 = md5.replace(/'/g, "''") const escapedMd5 = md5.replace(/'/g, "''")
// 用 md5 字段查询,获取 file_name
const sql = `SELECT file_name FROM video_hardlink_info_v4 WHERE md5 = '${escapedMd5}' LIMIT 1` const sql = `SELECT file_name FROM video_hardlink_info_v4 WHERE md5 = '${escapedMd5}' LIMIT 1`
const result = await wcdbService.execQuery('media', p, sql) const result = await wcdbService.execQuery('media', p, sql)
if (result.success && result.rows && result.rows.length > 0) { if (result.success && result.rows && result.rows.length > 0) {
const row = result.rows[0] const row = result.rows[0]
if (row?.file_name) { if (row?.file_name) {
// 提取不带扩展名的文件名作为实际视频 MD5
const realMd5 = String(row.file_name).replace(/\.[^.]+$/, '') const realMd5 = String(row.file_name).replace(/\.[^.]+$/, '')
this.log('加密 hardlink.db 命中', { file_name: row.file_name, realMd5 })
return realMd5 return realMd5
} }
} }
this.log('加密 hardlink.db 未命中', { path: p, result: JSON.stringify(result).slice(0, 200) })
} catch (e) { } catch (e) {
// 忽略错误 this.log('加密 hardlink.db 查询失败', { path: p, error: String(e) })
} }
} else {
this.log('加密 hardlink.db 不存在', { path: p })
} }
} }
} }
this.log('queryVideoFileName: 所有方法均未找到', { md5 })
return undefined return undefined
} }
@@ -170,12 +188,16 @@ class VideoService {
const dbPath = this.getDbPath() const dbPath = this.getDbPath()
const wxid = this.getMyWxid() const wxid = this.getMyWxid()
this.log('getVideoInfo 开始', { videoMd5, dbPath, wxid })
if (!dbPath || !wxid || !videoMd5) { if (!dbPath || !wxid || !videoMd5) {
this.log('getVideoInfo: 参数缺失', { dbPath: !!dbPath, wxid: !!wxid, videoMd5: !!videoMd5 })
return { exists: false } return { exists: false }
} }
// 先尝试从数据库查询真正的视频文件名 // 先尝试从数据库查询真正的视频文件名
const realVideoMd5 = await this.queryVideoFileName(videoMd5) || videoMd5 const realVideoMd5 = await this.queryVideoFileName(videoMd5) || videoMd5
this.log('realVideoMd5', { input: videoMd5, resolved: realVideoMd5, changed: realVideoMd5 !== videoMd5 })
// 检查 dbPath 是否已经包含 wxid避免重复拼接 // 检查 dbPath 是否已经包含 wxid避免重复拼接
const dbPathLower = dbPath.toLowerCase() const dbPathLower = dbPath.toLowerCase()
@@ -184,50 +206,58 @@ class VideoService {
let videoBaseDir: string let videoBaseDir: string
if (dbPathLower.includes(wxidLower) || dbPathLower.includes(cleanedWxid.toLowerCase())) { if (dbPathLower.includes(wxidLower) || dbPathLower.includes(cleanedWxid.toLowerCase())) {
// dbPath 已经包含 wxid直接使用
videoBaseDir = join(dbPath, 'msg', 'video') videoBaseDir = join(dbPath, 'msg', 'video')
} else { } else {
// dbPath 不包含 wxid需要拼接
videoBaseDir = join(dbPath, wxid, 'msg', 'video') videoBaseDir = join(dbPath, wxid, 'msg', 'video')
} }
this.log('videoBaseDir', { videoBaseDir, exists: existsSync(videoBaseDir) })
if (!existsSync(videoBaseDir)) { if (!existsSync(videoBaseDir)) {
this.log('getVideoInfo: videoBaseDir 不存在')
return { exists: false } return { exists: false }
} }
// 遍历年月目录查找视频文件 // 遍历年月目录查找视频文件
try { try {
const allDirs = readdirSync(videoBaseDir) const allDirs = readdirSync(videoBaseDir)
// 支持多种目录格式: YYYY-MM, YYYYMM, 或其他
const yearMonthDirs = allDirs const yearMonthDirs = allDirs
.filter(dir => { .filter(dir => {
const dirPath = join(videoBaseDir, dir) const dirPath = join(videoBaseDir, dir)
return statSync(dirPath).isDirectory() return statSync(dirPath).isDirectory()
}) })
.sort((a, b) => b.localeCompare(a)) // 从最新的目录开始查找 .sort((a, b) => b.localeCompare(a))
this.log('扫描目录', { dirs: yearMonthDirs })
for (const yearMonth of yearMonthDirs) { for (const yearMonth of yearMonthDirs) {
const dirPath = join(videoBaseDir, yearMonth) const dirPath = join(videoBaseDir, yearMonth)
const videoPath = join(dirPath, `${realVideoMd5}.mp4`) const videoPath = join(dirPath, `${realVideoMd5}.mp4`)
if (existsSync(videoPath)) {
this.log('找到视频', { videoPath })
const coverPath = join(dirPath, `${realVideoMd5}.jpg`) const coverPath = join(dirPath, `${realVideoMd5}.jpg`)
const thumbPath = join(dirPath, `${realVideoMd5}_thumb.jpg`) const thumbPath = join(dirPath, `${realVideoMd5}_thumb.jpg`)
// 检查视频文件是否存在
if (existsSync(videoPath)) {
return { return {
videoUrl: videoPath, // 返回文件路径,前端通过 readFile 读取 videoUrl: videoPath,
coverUrl: this.fileToDataUrl(coverPath, 'image/jpeg'), coverUrl: this.fileToDataUrl(coverPath, 'image/jpeg'),
thumbUrl: this.fileToDataUrl(thumbPath, 'image/jpeg'), thumbUrl: this.fileToDataUrl(thumbPath, 'image/jpeg'),
exists: true exists: true
} }
} }
} }
// 没找到,列出第一个目录里的文件帮助排查
if (yearMonthDirs.length > 0) {
const firstDir = join(videoBaseDir, yearMonthDirs[0])
const files = readdirSync(firstDir).filter(f => f.endsWith('.mp4')).slice(0, 5)
this.log('未找到视频,最新目录样本', { dir: yearMonthDirs[0], sampleFiles: files, lookingFor: `${realVideoMd5}.mp4` })
}
} catch (e) { } catch (e) {
// 忽略错误 this.log('getVideoInfo 遍历出错', { error: String(e) })
} }
this.log('getVideoInfo: 未找到视频', { videoMd5, realVideoMd5 })
return { exists: false } return { exists: false }
} }