mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 11:37:06 +08:00
修复bug,第一次导出缩略图,后面有了高清图后再次导出应该覆盖
按原图、中图和缩略图对本地图片资源分级,始终优先选择高清变体。 增量导出仅复用满足清晰度要求的图片,重新探测旧缩略图并在高清图出现后替换;仅缩略图降级时显示提示。
This commit is contained in:
@@ -23,6 +23,7 @@ import { getImageExportAttempts } from '../shared/export-media'
|
||||
import { FileAssetService } from './file-asset-service'
|
||||
import { mergeCachedSelfInfo } from './services/bootstrap-cache'
|
||||
import type { VoiceRecognitionUseCase } from './voice-pipeline/voice-recognition-use-case'
|
||||
import { imageFileQuality } from '../shared/image-quality'
|
||||
|
||||
const jobs = new Set<string>()
|
||||
const activeArchives = new Map<string, Archiver>()
|
||||
@@ -250,6 +251,7 @@ const mergeArchiveMessage = (previous: Message, current: Message): Message => {
|
||||
'exportMediaUrl',
|
||||
'exportMediaType',
|
||||
'exportMediaName',
|
||||
'exportMediaQuality',
|
||||
'exportAvatarUrl'
|
||||
]
|
||||
for (const key of preserveWhenMissing) {
|
||||
@@ -706,6 +708,7 @@ export async function runExport(
|
||||
message.exportMediaUrl = undefined
|
||||
message.exportMediaType = undefined
|
||||
message.exportMediaName = undefined
|
||||
message.exportMediaQuality = undefined
|
||||
message.exportMediaError = undefined
|
||||
message.voiceDataUrl = undefined
|
||||
message.voiceTranscript = undefined
|
||||
@@ -1063,15 +1066,20 @@ export async function runExport(
|
||||
: message.contentData.type === 'share' && message.contentData.typeVal === '6'
|
||||
? 'file'
|
||||
: null
|
||||
const reusableImageQuality =
|
||||
previous?.exportMediaQuality === 'original' ||
|
||||
(request.preferOriginal === false && previous?.exportMediaQuality === 'thumbnail')
|
||||
if (
|
||||
reusableMediaType &&
|
||||
previous?.exportMediaUrl &&
|
||||
(!previous.exportMediaType || previous.exportMediaType === reusableMediaType) &&
|
||||
(reusableMediaType !== 'image' || reusableImageQuality) &&
|
||||
(await resourceExists(previous.exportMediaUrl))
|
||||
) {
|
||||
message.exportMediaUrl = previous.exportMediaUrl
|
||||
message.exportMediaType = reusableMediaType
|
||||
message.exportMediaName = previous.exportMediaName
|
||||
message.exportMediaQuality = previous.exportMediaQuality
|
||||
send({
|
||||
jobId: request.jobId,
|
||||
phase: 'media',
|
||||
@@ -1087,7 +1095,6 @@ export async function runExport(
|
||||
} else {
|
||||
let fileFound = false
|
||||
let decryptedImage: { data: string; filePath: string } | null = null
|
||||
let usedFallback = false
|
||||
for (const attempt of getImageExportAttempts(request)) {
|
||||
const file = await imageService.findImageFileAsync(
|
||||
message.contentData.md5,
|
||||
@@ -1116,7 +1123,6 @@ export async function runExport(
|
||||
}
|
||||
if (!decrypted) continue
|
||||
decryptedImage = decrypted
|
||||
usedFallback = attempt.fallback || imageService.isThumbnailFile(decrypted.filePath)
|
||||
break
|
||||
}
|
||||
const decoded = decryptedImage ? decodeDataUrl(decryptedImage.data) : null
|
||||
@@ -1129,7 +1135,8 @@ export async function runExport(
|
||||
}
|
||||
message.exportMediaUrl = mediaUrl
|
||||
message.exportMediaType = 'image'
|
||||
if (usedFallback) {
|
||||
message.exportMediaQuality = imageFileQuality(decryptedImage!.filePath)
|
||||
if (request.preferOriginal !== false && message.exportMediaQuality === 'thumbnail') {
|
||||
keepMediaError(request, message, '原图不可用,已降级使用缩略图')
|
||||
}
|
||||
} else if (!fileFound) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { execFile } from 'child_process'
|
||||
import { Worker } from 'worker_threads'
|
||||
import ffmpegStaticPath from 'ffmpeg-static'
|
||||
import type { ImageDecoderSource, ImageDecoderStatus } from '../shared/image-decryption'
|
||||
import { imageFileQuality, imageQualityRank } from '../shared/image-quality'
|
||||
import { loadSettings } from './services/settings-store'
|
||||
import { Wcdb4Client } from './wcdb4-client'
|
||||
|
||||
@@ -208,6 +209,13 @@ function isThumbnailName(fileName) {
|
||||
return /(?:_t(?:_m)?|_thumb|\.thumb|_b|_w|_c)\.dat$/i.test(lower)
|
||||
}
|
||||
|
||||
function imageQualityRank(fileName) {
|
||||
const lower = fileName.toLowerCase()
|
||||
if (/(?:_t(?:_m)?|_thumb|\.thumb|_b|_w|_c)\.dat$/i.test(lower)) return 1
|
||||
if (/(?:_hd|\.hd|_h_m|_h|\.h)\.dat$/i.test(lower)) return 3
|
||||
return 2
|
||||
}
|
||||
|
||||
function buildPreferredDatNames(baseName) {
|
||||
const base = normalizeDatBase(baseName)
|
||||
if (!base) return []
|
||||
@@ -436,8 +444,8 @@ function collectCandidates(datPath, allowThumbnail) {
|
||||
.map((name) => path.join(directory, name))
|
||||
.filter((candidate) => fs.existsSync(candidate))
|
||||
.sort((left, right) => {
|
||||
const thumbnailOrder = Number(isThumbnailName(path.basename(left))) - Number(isThumbnailName(path.basename(right)))
|
||||
return thumbnailOrder || fs.statSync(right).size - fs.statSync(left).size
|
||||
const qualityOrder = imageQualityRank(path.basename(right)) - imageQualityRank(path.basename(left))
|
||||
return qualityOrder || fs.statSync(right).size - fs.statSync(left).size
|
||||
})
|
||||
return Array.from(new Set(candidates.concat(siblings)))
|
||||
}
|
||||
@@ -1824,17 +1832,17 @@ export class ImageDecryptService {
|
||||
.sort((left, right) => right.size - left.size)
|
||||
|
||||
const thumbnail = toSized(
|
||||
paths.filter((candidate) => this.isThumbnailName(basename(candidate)))
|
||||
paths.filter((candidate) => imageFileQuality(candidate) === 'thumbnail')
|
||||
)
|
||||
if (preferThumbnail && thumbnail[0]) return thumbnail[0].candidate
|
||||
const nonThumb = toSized(
|
||||
paths.filter((candidate) => !this.isThumbnailName(basename(candidate)))
|
||||
)
|
||||
if (nonThumb[0]) return nonThumb[0].candidate
|
||||
if (!allowThumbnail) return null
|
||||
|
||||
const existing = toSized(paths)
|
||||
return existing[0]?.candidate || null
|
||||
const allowed = toSized(paths)
|
||||
.filter((entry) => allowThumbnail || imageFileQuality(entry.candidate) !== 'thumbnail')
|
||||
.sort(
|
||||
(left, right) =>
|
||||
imageQualityRank(imageFileQuality(right.candidate)) -
|
||||
imageQualityRank(imageFileQuality(left.candidate)) || right.size - left.size
|
||||
)
|
||||
return allowed[0]?.candidate || null
|
||||
}
|
||||
|
||||
private async getLargestExistingPathAsync(
|
||||
@@ -1858,17 +1866,21 @@ export class ImageDecryptService {
|
||||
.sort((left, right) => right.size - left.size)
|
||||
|
||||
if (preferThumbnail) {
|
||||
const thumbnail = sized.find((entry) => this.isThumbnailName(basename(entry.candidate)))
|
||||
const thumbnail = sized.find((entry) => imageFileQuality(entry.candidate) === 'thumbnail')
|
||||
if (thumbnail) return thumbnail.candidate
|
||||
}
|
||||
const nonThumbnail = sized.find((entry) => !this.isThumbnailName(basename(entry.candidate)))
|
||||
if (nonThumbnail) return nonThumbnail.candidate
|
||||
return allowThumbnail ? sized[0]?.candidate || null : null
|
||||
const allowed = sized
|
||||
.filter((entry) => allowThumbnail || imageFileQuality(entry.candidate) !== 'thumbnail')
|
||||
.sort(
|
||||
(left, right) =>
|
||||
imageQualityRank(imageFileQuality(right.candidate)) -
|
||||
imageQualityRank(imageFileQuality(left.candidate)) || right.size - left.size
|
||||
)
|
||||
return allowed[0]?.candidate || null
|
||||
}
|
||||
|
||||
private isThumbnailName(fileName: string): boolean {
|
||||
const lower = fileName.toLowerCase()
|
||||
return /(?:_t(?:_m)?|_thumb|\.thumb|_b|_w|_c)\.dat$/i.test(lower)
|
||||
return imageFileQuality(fileName) === 'thumbnail'
|
||||
}
|
||||
|
||||
isThumbnailFile(filePath: string): boolean {
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
DatabaseKeyValidationResult
|
||||
} from '../../shared/database-key'
|
||||
import { mergeRecallArchiveMessages, recordRecallArchiveMessages } from './recall-archive-service'
|
||||
import type { ExportImageQuality } from '../../shared/image-quality'
|
||||
|
||||
export function getCurrentKey(): string {
|
||||
if (!dbRef) return ''
|
||||
@@ -60,6 +61,7 @@ export interface FormattedMessage {
|
||||
exportMediaUrl?: string
|
||||
exportMediaType?: 'image' | 'video' | 'sticker' | 'file'
|
||||
exportMediaName?: string
|
||||
exportMediaQuality?: ExportImageQuality
|
||||
exportShowAvatar?: boolean
|
||||
exportMediaError?: string
|
||||
exportAvatarUrl?: string
|
||||
|
||||
@@ -3,20 +3,17 @@ import type { ExportRequest } from './export'
|
||||
export type ImageExportAttempt = {
|
||||
allowThumbnail: boolean
|
||||
preferThumbnail: boolean
|
||||
fallback: boolean
|
||||
}
|
||||
|
||||
export function getImageExportAttempts(
|
||||
request: Pick<ExportRequest, 'preferOriginal' | 'fallbackThumbnail'>
|
||||
): ImageExportAttempt[] {
|
||||
if (request.preferOriginal === false) {
|
||||
return [{ allowThumbnail: true, preferThumbnail: true, fallback: false }]
|
||||
return [{ allowThumbnail: true, preferThumbnail: true }]
|
||||
}
|
||||
const attempts: ImageExportAttempt[] = [
|
||||
{ allowThumbnail: false, preferThumbnail: false, fallback: false }
|
||||
]
|
||||
const attempts: ImageExportAttempt[] = [{ allowThumbnail: false, preferThumbnail: false }]
|
||||
if (request.fallbackThumbnail !== false) {
|
||||
attempts.push({ allowThumbnail: true, preferThumbnail: true, fallback: true })
|
||||
attempts.push({ allowThumbnail: true, preferThumbnail: true })
|
||||
}
|
||||
return attempts
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export type ExportImageQuality = 'original' | 'medium' | 'thumbnail'
|
||||
|
||||
const fileNameOf = (filePath: string): string =>
|
||||
String(filePath || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[\\/]/)
|
||||
.pop() || ''
|
||||
|
||||
export const imageFileQuality = (filePath: string): ExportImageQuality => {
|
||||
const fileName = fileNameOf(filePath)
|
||||
if (!fileName.endsWith('.dat')) return 'original'
|
||||
if (/(?:_t(?:_m)?|_thumb|\.thumb|_b|_w|_c)\.dat$/i.test(fileName)) {
|
||||
return 'thumbnail'
|
||||
}
|
||||
if (/(?:_hd|\.hd|_h_m|_h|\.h)\.dat$/i.test(fileName)) return 'original'
|
||||
return 'medium'
|
||||
}
|
||||
|
||||
export const imageQualityRank = (quality: ExportImageQuality): number => {
|
||||
if (quality === 'original') return 3
|
||||
if (quality === 'medium') return 2
|
||||
return 1
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ExportImageQuality } from './image-quality'
|
||||
|
||||
export interface Contact {
|
||||
m_nsUsrName: string
|
||||
m_nsNickName: string
|
||||
@@ -36,6 +38,7 @@ export interface Message {
|
||||
exportMediaUrl?: string
|
||||
exportMediaType?: 'image' | 'video' | 'sticker' | 'file'
|
||||
exportMediaName?: string
|
||||
exportMediaQuality?: ExportImageQuality
|
||||
exportShowAvatar?: boolean
|
||||
exportMediaError?: string
|
||||
exportAvatarUrl?: string
|
||||
|
||||
Reference in New Issue
Block a user