diff --git a/db/migrations/20260722023032_add_artwork_tables.sql b/db/migrations/20260722023032_add_artwork_tables.sql index 7133b71ce..bc400f90e 100644 --- a/db/migrations/20260722023032_add_artwork_tables.sql +++ b/db/migrations/20260722023032_add_artwork_tables.sql @@ -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) diff --git a/model/artwork.go b/model/artwork.go index f5f3ab4db..7cb117764 100644 --- a/model/artwork.go +++ b/model/artwork.go @@ -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"` diff --git a/persistence/artwork_repository.go b/persistence/artwork_repository.go index 05912a0d6..505e3839d 100644 --- a/persistence/artwork_repository.go +++ b/persistence/artwork_repository.go @@ -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 diff --git a/persistence/artwork_repository_test.go b/persistence/artwork_repository_test.go index a13b1e7cb..82c2888c8 100644 --- a/persistence/artwork_repository_test.go +++ b/persistence/artwork_repository_test.go @@ -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()) })