fix(artwork): store backing-file provenance per item, not per hash

This commit is contained in:
Deluan 2026-07-22 15:52:54 -04:00
parent c04c8ee02a
commit 1f818e7633
4 changed files with 21 additions and 16 deletions

View File

@ -6,8 +6,6 @@ CREATE TABLE artwork (
height INTEGER NOT NULL DEFAULT 0,
size_bytes INTEGER NOT NULL DEFAULT 0,
blur_hash TEXT NOT NULL DEFAULT '',
source_path TEXT NOT NULL DEFAULT '',
ref_mtime INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@ -17,6 +15,8 @@ CREATE TABLE item_artwork (
image_type TEXT NOT NULL DEFAULT 'primary',
hash TEXT NOT NULL DEFAULT '',
source TEXT NOT NULL DEFAULT '',
source_path TEXT NOT NULL DEFAULT '',
ref_mtime INTEGER NOT NULL DEFAULT 0,
attempted_at TIMESTAMP,
updated_at TIMESTAMP,
PRIMARY KEY (item_kind, item_id, image_type)

View File

@ -4,15 +4,13 @@ import "time"
// Artwork is one unique image, identified by the XXH3-64 hash of its bytes.
type Artwork struct {
Hash string `structs:"hash"`
Mime string `structs:"mime"`
Width int `structs:"width"`
Height int `structs:"height"`
SizeBytes int64 `structs:"size_bytes"`
BlurHash string `structs:"blur_hash"`
SourcePath string `structs:"source_path"`
RefMtime int64 `structs:"ref_mtime"`
CreatedAt time.Time `structs:"created_at"`
Hash string `structs:"hash"`
Mime string `structs:"mime"`
Width int `structs:"width"`
Height int `structs:"height"`
SizeBytes int64 `structs:"size_bytes"`
BlurHash string `structs:"blur_hash"`
CreatedAt time.Time `structs:"created_at"`
}
const ImageTypePrimary = "primary"
@ -24,6 +22,10 @@ type ItemArtwork struct {
ImageType string `structs:"image_type"`
Hash string `structs:"hash"`
Source string `structs:"source"`
// SourcePath is the backing file (folder/upload: the image; embedded: the audio file); "" otherwise.
SourcePath string `structs:"source_path"`
// RefMtime is SourcePath's mtime at resolution; 0 when there is no SourcePath.
RefMtime int64 `structs:"ref_mtime"`
// attempted_at/updated_at are nullable in the schema but always set by PutItemArtwork;
// raw inserts must set them too, since these non-pointer time.Time fields fail to scan NULL.
AttemptedAt time.Time `structs:"attempted_at"`

View File

@ -53,8 +53,7 @@ func (r *artworkRepository) PutImage(a *model.Artwork) error {
}
// created_at=excluded.created_at: reacquiring an orphan must reset the prune grace window.
ins := Insert(r.tableName).SetMap(values).Suffix(`ON CONFLICT (hash) DO UPDATE SET mime=excluded.mime, width=excluded.width,
height=excluded.height, size_bytes=excluded.size_bytes, blur_hash=excluded.blur_hash,
source_path=excluded.source_path, ref_mtime=excluded.ref_mtime, created_at=excluded.created_at`)
height=excluded.height, size_bytes=excluded.size_bytes, blur_hash=excluded.blur_hash, created_at=excluded.created_at`)
_, err = r.executeSQL(ins)
return err
}
@ -139,7 +138,7 @@ func (r *artworkRepository) PutItemArtwork(ia *model.ItemArtwork) error {
return err
}
ins := Insert(itemArtworkTable).SetMap(values).Suffix(`ON CONFLICT (item_kind, item_id, image_type) DO UPDATE SET
hash=excluded.hash, source=excluded.source,
hash=excluded.hash, source=excluded.source, source_path=excluded.source_path, ref_mtime=excluded.ref_mtime,
attempted_at=excluded.attempted_at, updated_at=excluded.updated_at`)
_, err = r.items.executeSQL(ins)
return err

View File

@ -137,16 +137,20 @@ var _ = Describe("ArtworkRepository", func() {
})
Context("item state", func() {
It("upserts and reads state", func() {
It("upserts and reads state, including per-item provenance", func() {
ia := &model.ItemArtwork{ItemKind: "al", ItemID: "al1", ImageType: model.ImageTypePrimary,
Hash: "h1", Source: "folder", AttemptedAt: time.Now()}
Hash: "h1", Source: "folder", SourcePath: "/music/a/cover.jpg", RefMtime: 111, AttemptedAt: time.Now()}
Expect(repo.PutItemArtwork(ia)).To(Succeed())
ia.Source = "embedded"
ia.SourcePath = "/music/a/track.mp3"
ia.RefMtime = 222
Expect(repo.PutItemArtwork(ia)).To(Succeed())
got, err := repo.GetItemArtwork("al", "al1", model.ImageTypePrimary)
Expect(err).ToNot(HaveOccurred())
Expect(got.Source).To(Equal("embedded"))
Expect(got.SourcePath).To(Equal("/music/a/track.mp3"))
Expect(got.RefMtime).To(Equal(int64(222)))
Expect(got.UpdatedAt).ToNot(BeZero())
})