Kyoo/transcoder/src/fingerprints.go

161 lines
3.9 KiB
Go

package src
import (
"context"
"encoding/binary"
"errors"
"fmt"
"log/slog"
"github.com/jackc/pgx/v5"
"github.com/zoriya/kyoo/transcoder/src/exec"
"github.com/zoriya/kyoo/transcoder/src/utils"
)
const (
FingerprintVersion = 1
FpStartPercent = 0.20
FpStartDuration = 10 * 60
FpEndDuration = 5 * 60
)
type Fingerprint struct {
Start []uint32
End []uint32
}
func (s *MetadataService) ComputeFingerprint(ctx context.Context, info *MediaInfo) (*Fingerprint, error) {
if info.Versions.Fingerprint == FingerprintVersion {
var startData string
var endData string
err := s.Database.QueryRow(ctx,
`select start_data, end_data from gocoder.fingerprints where id = $1`,
info.Id,
).Scan(&startData, &endData)
if err == nil {
startFingerprint, err := DecompressFingerprint(startData)
if err != nil {
return nil, fmt.Errorf("failed to decompress start fingerprint: %w", err)
}
endFingerprint, err := DecompressFingerprint(endData)
if err != nil {
return nil, fmt.Errorf("failed to decompress end fingerprint: %w", err)
}
return &Fingerprint{
Start: startFingerprint,
End: endFingerprint,
}, nil
}
if !errors.Is(err, pgx.ErrNoRows) {
return nil, fmt.Errorf("failed to query fingerprint: %w", err)
}
}
get_running, set := s.fingerprintLock.Start(info.Sha)
if get_running != nil {
return get_running()
}
defer utils.PrintExecTime(ctx, "chromaprint for %s", info.Path)()
startFingerprint, err := computeChromaprint(
ctx,
info.Path,
0,
min(info.Duration*FpStartPercent, FpStartDuration),
)
if err != nil {
return set(nil, fmt.Errorf("failed to compute start fingerprint: %w", err))
}
endFingerprint, err := computeChromaprint(
ctx,
info.Path,
max(info.Duration-FpEndDuration, 0),
-1,
)
if err != nil {
return set(nil, fmt.Errorf("failed to compute end fingerprint: %w", err))
}
_, err = s.Database.Exec(ctx,
`update gocoder.info set ver_fingerprint = $2 where id = $1`,
info.Id,
FingerprintVersion,
)
if err != nil {
slog.ErrorContext(ctx, "failed to update fingerprint version", "path", info.Path, "err", err)
}
return set(&Fingerprint{
Start: startFingerprint,
End: endFingerprint,
}, nil)
}
func computeChromaprint(
ctx context.Context,
path string,
start float64,
duration float64,
) ([]uint32, error) {
ctx = context.WithoutCancel(ctx)
defer utils.PrintExecTime(ctx, "chromaprint for %s (between %f and %f)", path, start, duration)()
args := []string{
"-v", "error",
}
if start > 0 {
args = append(args, "-ss", fmt.Sprintf("%.6f", start))
}
if duration > 0 {
args = append(args, "-t", fmt.Sprintf("%.6f", duration))
}
args = append(args,
"-i", path,
"-ac", "2",
// this algorithm allows silence detection
"-algorithm", "3",
"-f", "chromaprint",
"-fp_format", "raw",
"-",
)
cmd := exec.CommandContext(
ctx,
"ffmpeg",
args...,
)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("ffmpeg failed: %w", err)
}
if len(output)%4 != 0 {
return nil, fmt.Errorf("invalid binary fingerprint size: %d", len(output))
}
result := make([]uint32, len(output)/4)
for i := range result {
result[i] = binary.LittleEndian.Uint32(output[i*4:])
}
return result, nil
}
func (s *MetadataService) StoreFingerprint(ctx context.Context, infoID int32, fingerprint *Fingerprint) error {
startCompressed, err := CompressFingerprint(fingerprint.Start)
if err != nil {
return fmt.Errorf("failed to compress start fingerprint: %w", err)
}
endCompressed, err := CompressFingerprint(fingerprint.End)
if err != nil {
return fmt.Errorf("failed to compress end fingerprint: %w", err)
}
_, err = s.Database.Exec(ctx,
`insert into gocoder.fingerprints(id, start_data, end_data) values ($1, $2, $3)
on conflict (id) do update set start_data = excluded.start_data, end_data = excluded.end_data`,
infoID, startCompressed, endCompressed,
)
return err
}