Computes a SHA-256 partial-content fingerprint (file size + first 64 KB) for movies, TV episodes, and mixed files during scans. When a file is moved or renamed within a library, the scan detects the fingerprint match, renames the media_items row in-place, and updates media_tags.media_key to match — so tags and NFO metadata survive the move transparently. - src/lib/fingerprint.ts: new computeFingerprint() using sync FS reads - src/lib/db.ts: fingerprint TEXT column + index migration - src/lib/tags.ts: reKeyMediaItem() to update media_tags on rename - src/lib/scanner.ts: replace clear+upsert with detectMoves/reconcileAndPrune for movies, TV episodes, and mixed files; games retain clear+upsert (v1) - TV scan restructured to a single filesystem pass (no double-scanning) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
37 lines
975 B
TypeScript
37 lines
975 B
TypeScript
import fs from 'fs'
|
|
import crypto from 'crypto'
|
|
|
|
const CHUNK_SIZE = 64 * 1024 // 64 KB
|
|
|
|
/**
|
|
* Computes a stable partial-content fingerprint for a file.
|
|
* Uses SHA-256 of the file size + first 64 KB of content.
|
|
* Fast enough for large video files (~instant) and collision-resistant
|
|
* for real-world media libraries.
|
|
*
|
|
* Returns null if the file cannot be read (missing, permission error, etc.).
|
|
*/
|
|
export function computeFingerprint(absolutePath: string): string | null {
|
|
try {
|
|
const stat = fs.statSync(absolutePath)
|
|
const size = stat.size
|
|
const chunkLen = Math.min(CHUNK_SIZE, size)
|
|
const buf = Buffer.alloc(chunkLen)
|
|
if (chunkLen > 0) {
|
|
const fd = fs.openSync(absolutePath, 'r')
|
|
try {
|
|
fs.readSync(fd, buf, 0, chunkLen, 0)
|
|
} finally {
|
|
fs.closeSync(fd)
|
|
}
|
|
}
|
|
return crypto
|
|
.createHash('sha256')
|
|
.update(`${size}:`)
|
|
.update(buf)
|
|
.digest('hex')
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|