handle video tagging

This commit is contained in:
Garret Patti
2026-04-12 17:24:39 -04:00
parent ad9920a448
commit 6c769b457f
6 changed files with 178 additions and 52 deletions

View File

@@ -87,22 +87,13 @@ async function getVideoDuration(src: string): Promise<number> {
})
}
/** Generate a thumbnail from a video using ffmpeg. */
async function generateVideoThumbnail(src: string, dest: string): Promise<void> {
/** Extract a single frame from a video at the given offset (seconds) and write to dest. */
async function generateVideoFrameAtOffset(src: string, dest: string, offsetSeconds: number): Promise<void> {
const tmp = dest + '.tmp'
// Seek to 10% of the video duration for a representative frame
let offset = 0
try {
const duration = await getVideoDuration(src)
offset = Math.max(0, duration * 0.1)
} catch {
// If ffprobe fails, fall back to seeking to 0
}
const args = [
'-y', // overwrite output
'-ss', String(offset), // seek before input (fast)
'-ss', String(offsetSeconds), // seek before input (fast)
'-i', src,
'-frames:v', '1',
'-q:v', '5',
@@ -115,6 +106,58 @@ async function generateVideoThumbnail(src: string, dest: string): Promise<void>
fs.renameSync(tmp, dest)
}
/** Generate a thumbnail from a video using ffmpeg (seeks to 10% of duration). */
async function generateVideoThumbnail(src: string, dest: string): Promise<void> {
let offset = 0
try {
const duration = await getVideoDuration(src)
offset = Math.max(0, duration * 0.1)
} catch {
// If ffprobe fails, fall back to seeking to 0
}
await generateVideoFrameAtOffset(src, dest, offset)
}
/**
* Extract frames from a video at each given percentage of its duration.
* Returns the absolute paths to the cached frame JPEGs, in the same order as `percentages`.
* Uses a per-frame cache key so each frame is cached independently.
*/
export async function getVideoFramePaths(
absoluteFilePath: string,
libraryId: string,
percentages: number[]
): Promise<string[]> {
ensureCacheDir()
let duration = 0
try {
duration = await getVideoDuration(absoluteFilePath)
} catch {
// Fall back to 0; all frames will seek to position 0
}
const framePaths: string[] = []
for (const pct of percentages) {
const offset = Math.max(0, duration * pct)
const key = crypto
.createHash('sha1')
.update(libraryId + ':' + absoluteFilePath + ':' + pct)
.digest('hex')
const cacheFile = path.join(CACHE_DIR, key + '.jpg')
const cached = getCachedPath(cacheFile, absoluteFilePath)
if (!cached) {
await generateVideoFrameAtOffset(absoluteFilePath, cacheFile, offset)
}
framePaths.push(cacheFile)
}
return framePaths
}
/**
* Returns the absolute path to a cached thumbnail JPEG for the given file.
* Generates it on first call (or when the source has been modified).