Add file fingerprinting for move-resilient media item identity
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>
This commit is contained in:
36
src/lib/fingerprint.ts
Normal file
36
src/lib/fingerprint.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user