This commit is contained in:
xuncha
2026-03-13 20:40:16 +08:00
committed by xuncha
parent c9a6451407
commit 53ef4e11f9
2 changed files with 187 additions and 133 deletions

View File

@@ -10,7 +10,7 @@ type WorkerPayload = {
thumbOnly: boolean thumbOnly: boolean
} }
type Candidate = { score: number; path: string; isThumb: boolean; hasX: boolean } type Candidate = { score: number; path: string; isThumb: boolean }
const payload = workerData as WorkerPayload const payload = workerData as WorkerPayload
@@ -18,16 +18,26 @@ function looksLikeMd5(value: string): boolean {
return /^[a-fA-F0-9]{16,32}$/.test(value) return /^[a-fA-F0-9]{16,32}$/.test(value)
} }
function stripDatVariantSuffix(base: string): string {
const lower = base.toLowerCase()
const suffixes = ['_thumb', '.thumb', '_hd', '.hd', '_h', '.h', '_t', '.t', '_c', '.c']
for (const suffix of suffixes) {
if (lower.endsWith(suffix)) {
return lower.slice(0, -suffix.length)
}
}
if (/[._][a-z]$/.test(lower)) {
return lower.slice(0, -2)
}
return lower
}
function hasXVariant(baseLower: string): boolean { function hasXVariant(baseLower: string): boolean {
return /[._][a-z]$/.test(baseLower) return stripDatVariantSuffix(baseLower) !== baseLower
} }
function hasImageVariantSuffix(baseLower: string): boolean { function hasImageVariantSuffix(baseLower: string): boolean {
return /[._][a-z]$/.test(baseLower) return stripDatVariantSuffix(baseLower) !== baseLower
}
function isLikelyImageDatBase(baseLower: string): boolean {
return hasImageVariantSuffix(baseLower) || looksLikeMd5(baseLower)
} }
function normalizeDatBase(name: string): string { function normalizeDatBase(name: string): string {
@@ -35,10 +45,17 @@ function normalizeDatBase(name: string): string {
if (base.endsWith('.dat') || base.endsWith('.jpg')) { if (base.endsWith('.dat') || base.endsWith('.jpg')) {
base = base.slice(0, -4) base = base.slice(0, -4)
} }
while (/[._][a-z]$/.test(base)) { while (true) {
base = base.slice(0, -2) const stripped = stripDatVariantSuffix(base)
} if (stripped === base) {
return base return base
}
base = stripped
}
}
function isLikelyImageDatBase(baseLower: string): boolean {
return hasImageVariantSuffix(baseLower) || looksLikeMd5(normalizeDatBase(baseLower))
} }
function matchesDatName(fileName: string, datName: string): boolean { function matchesDatName(fileName: string, datName: string): boolean {
@@ -47,25 +64,23 @@ function matchesDatName(fileName: string, datName: string): boolean {
const normalizedBase = normalizeDatBase(base) const normalizedBase = normalizeDatBase(base)
const normalizedTarget = normalizeDatBase(datName.toLowerCase()) const normalizedTarget = normalizeDatBase(datName.toLowerCase())
if (normalizedBase === normalizedTarget) return true if (normalizedBase === normalizedTarget) return true
const pattern = new RegExp(`^${datName}(?:[._][a-z])?\\.dat$`) return lower.endsWith('.dat') && lower.includes(normalizedTarget)
if (pattern.test(lower)) return true
return lower.endsWith('.dat') && lower.includes(datName)
} }
function scoreDatName(fileName: string): number { function scoreDatName(fileName: string): number {
if (fileName.includes('.t.dat') || fileName.includes('_t.dat')) return 1 const lower = fileName.toLowerCase()
if (fileName.includes('.c.dat') || fileName.includes('_c.dat')) return 1 const baseLower = lower.endsWith('.dat') ? lower.slice(0, -4) : lower
return 2 if (baseLower.endsWith('_h') || baseLower.endsWith('.h')) return 600
if (!hasXVariant(baseLower)) return 500
if (baseLower.endsWith('_hd') || baseLower.endsWith('.hd')) return 450
if (baseLower.endsWith('_c') || baseLower.endsWith('.c')) return 400
if (isThumbnailDat(lower)) return 100
return 350
} }
function isThumbnailDat(fileName: string): boolean { function isThumbnailDat(fileName: string): boolean {
return fileName.includes('.t.dat') || fileName.includes('_t.dat')
}
function isHdDat(fileName: string): boolean {
const lower = fileName.toLowerCase() const lower = fileName.toLowerCase()
const base = lower.endsWith('.dat') ? lower.slice(0, -4) : lower return lower.includes('.t.dat') || lower.includes('_t.dat') || lower.includes('_thumb.dat')
return base.endsWith('_hd') || base.endsWith('_h')
} }
function walkForDat( function walkForDat(
@@ -105,20 +120,15 @@ function walkForDat(
if (!lower.endsWith('.dat')) continue if (!lower.endsWith('.dat')) continue
const baseLower = lower.slice(0, -4) const baseLower = lower.slice(0, -4)
if (!isLikelyImageDatBase(baseLower)) continue if (!isLikelyImageDatBase(baseLower)) continue
if (!hasXVariant(baseLower)) continue
if (!matchesDatName(lower, datName)) continue if (!matchesDatName(lower, datName)) continue
// 排除高清图片格式 (_hd, _h)
if (isHdDat(lower)) continue
matchedBases.add(baseLower) matchedBases.add(baseLower)
const isThumb = isThumbnailDat(lower) const isThumb = isThumbnailDat(lower)
if (!allowThumbnail && isThumb) continue if (!allowThumbnail && isThumb) continue
if (thumbOnly && !isThumb) continue if (thumbOnly && !isThumb) continue
const score = scoreDatName(lower)
candidates.push({ candidates.push({
score, score: scoreDatName(lower),
path: entryPath, path: entryPath,
isThumb, isThumb
hasX: hasXVariant(baseLower)
}) })
} }
} }
@@ -126,10 +136,8 @@ function walkForDat(
return { path: null, matchedBases: Array.from(matchedBases).slice(0, 20) } return { path: null, matchedBases: Array.from(matchedBases).slice(0, 20) }
} }
const withX = candidates.filter((item) => item.hasX) const nonThumb = candidates.filter((item) => !item.isThumb)
const basePool = withX.length ? withX : candidates const finalPool = thumbOnly ? candidates : (nonThumb.length ? nonThumb : candidates)
const nonThumb = basePool.filter((item) => !item.isThumb)
const finalPool = thumbOnly ? basePool : (nonThumb.length ? nonThumb : basePool)
let best: { score: number; path: string } | null = null let best: { score: number; path: string } | null = null
for (const item of finalPool) { for (const item of finalPool) {

View File

@@ -414,23 +414,33 @@ export class ImageDecryptService {
if (!skipResolvedCache) { if (!skipResolvedCache) {
if (imageMd5) { if (imageMd5) {
const cached = this.resolvedCache.get(imageMd5) const cached = this.resolvedCache.get(imageMd5)
if (cached && existsSync(cached)) return cached if (cached && existsSync(cached)) {
const preferred = this.getPreferredDatVariantPath(cached, allowThumbnail)
this.cacheDatPath(accountDir, imageMd5, preferred)
if (imageDatName) this.cacheDatPath(accountDir, imageDatName, preferred)
return preferred
}
} }
if (imageDatName) { if (imageDatName) {
const cached = this.resolvedCache.get(imageDatName) const cached = this.resolvedCache.get(imageDatName)
if (cached && existsSync(cached)) return cached if (cached && existsSync(cached)) {
const preferred = this.getPreferredDatVariantPath(cached, allowThumbnail)
this.cacheDatPath(accountDir, imageDatName, preferred)
if (imageMd5) this.cacheDatPath(accountDir, imageMd5, preferred)
return preferred
}
} }
} }
// 1. 通过 MD5 快速定位 (MsgAttach 目录) // 1. 通过 MD5 快速定位 (MsgAttach 目录)
if (imageMd5) { if (imageMd5) {
const res = await this.fastProbabilisticSearch(accountDir, imageMd5, allowThumbnail) const res = await this.fastProbabilisticSearch(join(accountDir, 'msg', 'attach'), imageMd5, allowThumbnail)
if (res) return res if (res) return res
} }
// 2. 如果 imageDatName 看起来像 MD5也尝试快速定位 // 2. 如果 imageDatName 看起来像 MD5也尝试快速定位
if (!imageMd5 && imageDatName && this.looksLikeMd5(imageDatName)) { if (!imageMd5 && imageDatName && this.looksLikeMd5(imageDatName)) {
const res = await this.fastProbabilisticSearch(accountDir, imageDatName, allowThumbnail) const res = await this.fastProbabilisticSearch(join(accountDir, 'msg', 'attach'), imageDatName, allowThumbnail)
if (res) return res if (res) return res
} }
@@ -439,16 +449,17 @@ export class ImageDecryptService {
this.logInfo('[ImageDecrypt] hardlink lookup (md5)', { imageMd5, sessionId }) this.logInfo('[ImageDecrypt] hardlink lookup (md5)', { imageMd5, sessionId })
const hardlinkPath = await this.resolveHardlinkPath(accountDir, imageMd5, sessionId) const hardlinkPath = await this.resolveHardlinkPath(accountDir, imageMd5, sessionId)
if (hardlinkPath) { if (hardlinkPath) {
const isThumb = this.isThumbnailPath(hardlinkPath) const preferredPath = this.getPreferredDatVariantPath(hardlinkPath, allowThumbnail)
const isThumb = this.isThumbnailPath(preferredPath)
if (allowThumbnail || !isThumb) { if (allowThumbnail || !isThumb) {
this.logInfo('[ImageDecrypt] hardlink hit', { imageMd5, path: hardlinkPath }) this.logInfo('[ImageDecrypt] hardlink hit', { imageMd5, path: preferredPath })
this.cacheDatPath(accountDir, imageMd5, hardlinkPath) this.cacheDatPath(accountDir, imageMd5, preferredPath)
if (imageDatName) this.cacheDatPath(accountDir, imageDatName, hardlinkPath) if (imageDatName) this.cacheDatPath(accountDir, imageDatName, preferredPath)
return hardlinkPath return preferredPath
} }
// hardlink 找到的是缩略图,但要求高清图 // hardlink 找到的是缩略图,但要求高清图
// 尝试在同一目录下查找高清图变体(快速查找,不遍历) // 尝试在同一目录下查找高清图变体(快速查找,不遍历)
const hdPath = this.findHdVariantInSameDir(hardlinkPath) const hdPath = this.findHdVariantInSameDir(preferredPath)
if (hdPath) { if (hdPath) {
this.cacheDatPath(accountDir, imageMd5, hdPath) this.cacheDatPath(accountDir, imageMd5, hdPath)
if (imageDatName) this.cacheDatPath(accountDir, imageDatName, hdPath) if (imageDatName) this.cacheDatPath(accountDir, imageDatName, hdPath)
@@ -462,16 +473,19 @@ export class ImageDecryptService {
this.logInfo('[ImageDecrypt] hardlink fallback (datName)', { imageDatName, sessionId }) this.logInfo('[ImageDecrypt] hardlink fallback (datName)', { imageDatName, sessionId })
const fallbackPath = await this.resolveHardlinkPath(accountDir, imageDatName, sessionId) const fallbackPath = await this.resolveHardlinkPath(accountDir, imageDatName, sessionId)
if (fallbackPath) { if (fallbackPath) {
const isThumb = this.isThumbnailPath(fallbackPath) const preferredPath = this.getPreferredDatVariantPath(fallbackPath, allowThumbnail)
const isThumb = this.isThumbnailPath(preferredPath)
if (allowThumbnail || !isThumb) { if (allowThumbnail || !isThumb) {
this.logInfo('[ImageDecrypt] hardlink hit (datName)', { imageMd5: imageDatName, path: fallbackPath }) this.logInfo('[ImageDecrypt] hardlink hit (datName)', { imageMd5: imageDatName, path: preferredPath })
this.cacheDatPath(accountDir, imageDatName, fallbackPath) this.cacheDatPath(accountDir, imageDatName, preferredPath)
return fallbackPath if (imageMd5) this.cacheDatPath(accountDir, imageMd5, preferredPath)
return preferredPath
} }
// 找到缩略图但要求高清图,尝试同目录查找高清图变体 // 找到缩略图但要求高清图,尝试同目录查找高清图变体
const hdPath = this.findHdVariantInSameDir(fallbackPath) const hdPath = this.findHdVariantInSameDir(preferredPath)
if (hdPath) { if (hdPath) {
this.cacheDatPath(accountDir, imageDatName, hdPath) this.cacheDatPath(accountDir, imageDatName, hdPath)
if (imageMd5) this.cacheDatPath(accountDir, imageMd5, hdPath)
return hdPath return hdPath
} }
return null return null
@@ -484,14 +498,15 @@ export class ImageDecryptService {
this.logInfo('[ImageDecrypt] hardlink lookup (datName)', { imageDatName, sessionId }) this.logInfo('[ImageDecrypt] hardlink lookup (datName)', { imageDatName, sessionId })
const hardlinkPath = await this.resolveHardlinkPath(accountDir, imageDatName, sessionId) const hardlinkPath = await this.resolveHardlinkPath(accountDir, imageDatName, sessionId)
if (hardlinkPath) { if (hardlinkPath) {
const isThumb = this.isThumbnailPath(hardlinkPath) const preferredPath = this.getPreferredDatVariantPath(hardlinkPath, allowThumbnail)
const isThumb = this.isThumbnailPath(preferredPath)
if (allowThumbnail || !isThumb) { if (allowThumbnail || !isThumb) {
this.logInfo('[ImageDecrypt] hardlink hit', { imageMd5: imageDatName, path: hardlinkPath }) this.logInfo('[ImageDecrypt] hardlink hit', { imageMd5: imageDatName, path: preferredPath })
this.cacheDatPath(accountDir, imageDatName, hardlinkPath) this.cacheDatPath(accountDir, imageDatName, preferredPath)
return hardlinkPath return preferredPath
} }
// hardlink 找到的是缩略图,但要求高清图 // hardlink 找到的是缩略图,但要求高清图
const hdPath = this.findHdVariantInSameDir(hardlinkPath) const hdPath = this.findHdVariantInSameDir(preferredPath)
if (hdPath) { if (hdPath) {
this.cacheDatPath(accountDir, imageDatName, hdPath) this.cacheDatPath(accountDir, imageDatName, hdPath)
return hdPath return hdPath
@@ -510,9 +525,10 @@ export class ImageDecryptService {
if (!skipResolvedCache) { if (!skipResolvedCache) {
const cached = this.resolvedCache.get(imageDatName) const cached = this.resolvedCache.get(imageDatName)
if (cached && existsSync(cached)) { if (cached && existsSync(cached)) {
if (allowThumbnail || !this.isThumbnailPath(cached)) return cached const preferred = this.getPreferredDatVariantPath(cached, allowThumbnail)
if (allowThumbnail || !this.isThumbnailPath(preferred)) return preferred
// 缓存的是缩略图,尝试找高清图 // 缓存的是缩略图,尝试找高清图
const hdPath = this.findHdVariantInSameDir(cached) const hdPath = this.findHdVariantInSameDir(preferred)
if (hdPath) return hdPath if (hdPath) return hdPath
} }
} }
@@ -801,7 +817,8 @@ export class ImageDecryptService {
const key = `${accountDir}|${datName}` const key = `${accountDir}|${datName}`
const cached = this.resolvedCache.get(key) const cached = this.resolvedCache.get(key)
if (cached && existsSync(cached)) { if (cached && existsSync(cached)) {
if (allowThumbnail || !this.isThumbnailPath(cached)) return cached const preferred = this.getPreferredDatVariantPath(cached, allowThumbnail)
if (allowThumbnail || !this.isThumbnailPath(preferred)) return preferred
} }
const root = join(accountDir, 'msg', 'attach') const root = join(accountDir, 'msg', 'attach')
@@ -810,7 +827,7 @@ export class ImageDecryptService {
// 优化1快速概率性查找 // 优化1快速概率性查找
// 包含1. 基于文件名的前缀猜测 (旧版) // 包含1. 基于文件名的前缀猜测 (旧版)
// 2. 基于日期的最近月份扫描 (新版无索引时) // 2. 基于日期的最近月份扫描 (新版无索引时)
const fastHit = await this.fastProbabilisticSearch(root, datName) const fastHit = await this.fastProbabilisticSearch(root, datName, allowThumbnail)
if (fastHit) { if (fastHit) {
this.resolvedCache.set(key, fastHit) this.resolvedCache.set(key, fastHit)
return fastHit return fastHit
@@ -830,34 +847,29 @@ export class ImageDecryptService {
* 包含1. 微信旧版结构 filename.substr(0, 2)/... * 包含1. 微信旧版结构 filename.substr(0, 2)/...
* 2. 微信新版结构 msg/attach/{hash}/{YYYY-MM}/Img/filename * 2. 微信新版结构 msg/attach/{hash}/{YYYY-MM}/Img/filename
*/ */
private async fastProbabilisticSearch(root: string, datName: string, _allowThumbnail?: boolean): Promise<string | null> { private async fastProbabilisticSearch(root: string, datName: string, allowThumbnail = true): Promise<string | null> {
const { promises: fs } = require('fs') const { promises: fs } = require('fs')
const { join } = require('path') const { join } = require('path')
try { try {
// --- 策略 A: 旧版路径猜测 (msg/attach/xx/yy/...) --- // --- 策略 A: 旧版路径猜测 (msg/attach/xx/yy/...) ---
const lowerName = datName.toLowerCase() const lowerName = datName.toLowerCase()
let baseName = lowerName const baseName = this.normalizeDatBase(lowerName)
if (baseName.endsWith('.dat')) { const targetNames = this.buildPreferredDatNames(baseName, allowThumbnail)
baseName = baseName.slice(0, -4)
if (baseName.endsWith('_t') || baseName.endsWith('.t') || baseName.endsWith('_hd')) {
baseName = baseName.slice(0, -3)
} else if (baseName.endsWith('_thumb')) {
baseName = baseName.slice(0, -6)
}
}
const candidates: string[] = [] const candidates: string[] = []
if (/^[a-f0-9]{32}$/.test(baseName)) { if (/^[a-f0-9]{32}$/.test(baseName)) {
const dir1 = baseName.substring(0, 2) const dir1 = baseName.substring(0, 2)
const dir2 = baseName.substring(2, 4) const dir2 = baseName.substring(2, 4)
for (const targetName of targetNames) {
candidates.push( candidates.push(
join(root, dir1, dir2, datName), join(root, dir1, dir2, targetName),
join(root, dir1, dir2, 'Img', datName), join(root, dir1, dir2, 'Img', targetName),
join(root, dir1, dir2, 'mg', datName), join(root, dir1, dir2, 'mg', targetName),
join(root, dir1, dir2, 'Image', datName) join(root, dir1, dir2, 'Image', targetName)
) )
} }
}
for (const path of candidates) { for (const path of candidates) {
try { try {
@@ -883,13 +895,6 @@ export class ImageDecryptService {
months.push(mStr) months.push(mStr)
} }
const targetNames = [datName]
if (baseName !== lowerName) {
targetNames.push(`${baseName}.dat`)
targetNames.push(`${baseName}_t.dat`)
targetNames.push(`${baseName}_thumb.dat`)
}
const batchSize = 20 const batchSize = 20
for (let i = 0; i < sessionDirs.length; i += batchSize) { for (let i = 0; i < sessionDirs.length; i += batchSize) {
const batch = sessionDirs.slice(i, i + batchSize) const batch = sessionDirs.slice(i, i + batchSize)
@@ -919,36 +924,13 @@ export class ImageDecryptService {
/** /**
* 在同一目录下查找高清图变体 * 在同一目录下查找高清图变体
* 缩略图 xxx_t.dat -> 高清图 xxx_h.dat 或 xxx.dat * 优先 `_h`,再回退其他非缩略图变体
*/ */
private findHdVariantInSameDir(thumbPath: string): string | null { private findHdVariantInSameDir(thumbPath: string): string | null {
try { try {
const dir = dirname(thumbPath) const dir = dirname(thumbPath)
const fileName = basename(thumbPath).toLowerCase() const fileName = basename(thumbPath)
return this.findPreferredDatVariantInDir(dir, fileName, false)
// 提取基础名称(去掉 _t.dat 或 .t.dat
let baseName = fileName
if (baseName.endsWith('_t.dat')) {
baseName = baseName.slice(0, -6)
} else if (baseName.endsWith('.t.dat')) {
baseName = baseName.slice(0, -6)
} else {
return null
}
// 尝试查找高清图变体
const variants = [
`${baseName}_h.dat`,
`${baseName}.h.dat`,
`${baseName}.dat`
]
for (const variant of variants) {
const variantPath = join(dir, variant)
if (existsSync(variantPath)) {
return variantPath
}
}
} catch { } } catch { }
return null return null
} }
@@ -1001,23 +983,105 @@ export class ImageDecryptService {
}) })
} }
private stripDatVariantSuffix(base: string): string {
const lower = base.toLowerCase()
const suffixes = ['_thumb', '.thumb', '_hd', '.hd', '_h', '.h', '_t', '.t', '_c', '.c']
for (const suffix of suffixes) {
if (lower.endsWith(suffix)) {
return lower.slice(0, -suffix.length)
}
}
if (/[._][a-z]$/.test(lower)) {
return lower.slice(0, -2)
}
return lower
}
private getDatVariantPriority(name: string): number {
const lower = name.toLowerCase()
const baseLower = lower.endsWith('.dat') || lower.endsWith('.jpg') ? lower.slice(0, -4) : lower
if (baseLower.endsWith('_h') || baseLower.endsWith('.h')) return 600
if (!this.hasXVariant(baseLower)) return 500
if (baseLower.endsWith('_hd') || baseLower.endsWith('.hd')) return 450
if (baseLower.endsWith('_c') || baseLower.endsWith('.c')) return 400
if (this.isThumbnailDat(lower)) return 100
return 350
}
private buildPreferredDatNames(baseName: string, allowThumbnail: boolean): string[] {
if (!baseName) return []
const names = [
`${baseName}_h.dat`,
`${baseName}.h.dat`,
`${baseName}.dat`,
`${baseName}_hd.dat`,
`${baseName}.hd.dat`,
`${baseName}_c.dat`,
`${baseName}.c.dat`
]
if (allowThumbnail) {
names.push(
`${baseName}_thumb.dat`,
`${baseName}.thumb.dat`,
`${baseName}_t.dat`,
`${baseName}.t.dat`
)
}
return Array.from(new Set(names))
}
private findPreferredDatVariantInDir(dirPath: string, baseName: string, allowThumbnail: boolean): string | null {
let entries: string[]
try {
entries = readdirSync(dirPath)
} catch {
return null
}
const target = this.normalizeDatBase(baseName.toLowerCase())
let bestPath: string | null = null
let bestScore = Number.NEGATIVE_INFINITY
for (const entry of entries) {
const lower = entry.toLowerCase()
if (!lower.endsWith('.dat')) continue
if (!allowThumbnail && this.isThumbnailDat(lower)) continue
const baseLower = lower.slice(0, -4)
if (this.normalizeDatBase(baseLower) !== target) continue
const score = this.getDatVariantPriority(lower)
if (score > bestScore) {
bestScore = score
bestPath = join(dirPath, entry)
}
}
return bestPath
}
private getPreferredDatVariantPath(datPath: string, allowThumbnail: boolean): string {
const lower = datPath.toLowerCase()
if (!lower.endsWith('.dat')) return datPath
const preferred = this.findPreferredDatVariantInDir(dirname(datPath), basename(datPath), allowThumbnail)
return preferred || datPath
}
private normalizeDatBase(name: string): string { private normalizeDatBase(name: string): string {
let base = name.toLowerCase() let base = name.toLowerCase()
if (base.endsWith('.dat') || base.endsWith('.jpg')) { if (base.endsWith('.dat') || base.endsWith('.jpg')) {
base = base.slice(0, -4) base = base.slice(0, -4)
} }
while (/[._][a-z]$/.test(base)) { for (;;) {
base = base.slice(0, -2) const stripped = this.stripDatVariantSuffix(base)
} if (stripped === base) {
return base return base
} }
base = stripped
}
}
private hasImageVariantSuffix(baseLower: string): boolean { private hasImageVariantSuffix(baseLower: string): boolean {
return /[._][a-z]$/.test(baseLower) return this.stripDatVariantSuffix(baseLower) !== baseLower
} }
private isLikelyImageDatBase(baseLower: string): boolean { private isLikelyImageDatBase(baseLower: string): boolean {
return this.hasImageVariantSuffix(baseLower) || this.looksLikeMd5(baseLower) return this.hasImageVariantSuffix(baseLower) || this.looksLikeMd5(this.normalizeDatBase(baseLower))
} }
@@ -1206,24 +1270,7 @@ export class ImageDecryptService {
} }
private findNonThumbnailVariantInDir(dirPath: string, baseName: string): string | null { private findNonThumbnailVariantInDir(dirPath: string, baseName: string): string | null {
let entries: string[] return this.findPreferredDatVariantInDir(dirPath, baseName, false)
try {
entries = readdirSync(dirPath)
} catch {
return null
}
const target = this.normalizeDatBase(baseName.toLowerCase())
for (const entry of entries) {
const lower = entry.toLowerCase()
if (!lower.endsWith('.dat')) continue
if (this.isThumbnailDat(lower)) continue
const baseLower = lower.slice(0, -4)
// 只排除没有 _x 变体后缀的文件(允许 _hd、_h 等所有带变体的)
if (!this.hasXVariant(baseLower)) continue
if (this.normalizeDatBase(baseLower) !== target) continue
return join(dirPath, entry)
}
return null
} }
private isNonThumbnailVariantDat(datPath: string): boolean { private isNonThumbnailVariantDat(datPath: string): boolean {
@@ -1231,8 +1278,7 @@ export class ImageDecryptService {
if (!lower.endsWith('.dat')) return false if (!lower.endsWith('.dat')) return false
if (this.isThumbnailDat(lower)) return false if (this.isThumbnailDat(lower)) return false
const baseLower = lower.slice(0, -4) const baseLower = lower.slice(0, -4)
// 只检查是否有 _x 变体后缀(允许 _hd、_h 等所有带变体的) return this.isLikelyImageDatBase(baseLower)
return this.hasXVariant(baseLower)
} }
private emitImageUpdate(payload: { sessionId?: string; imageMd5?: string; imageDatName?: string }, cacheKey: string): void { private emitImageUpdate(payload: { sessionId?: string; imageMd5?: string; imageDatName?: string }, cacheKey: string): void {
@@ -1858,7 +1904,7 @@ export class ImageDecryptService {
private hasXVariant(base: string): boolean { private hasXVariant(base: string): boolean {
const lower = base.toLowerCase() const lower = base.toLowerCase()
return lower.endsWith('_h') || lower.endsWith('_hd') || lower.endsWith('_thumb') || lower.endsWith('_t') return this.stripDatVariantSuffix(lower) !== lower
} }
private isHdPath(p: string): boolean { private isHdPath(p: string): boolean {