feat(store): add kind column splitting manga and novel libraries
This commit is contained in:
@@ -33,6 +33,9 @@ type Bookmark struct {
|
||||
// Archived series stay polled for new chapters; finished ones do not.
|
||||
// Empty on the way in means "no opinion" — see Upsert.
|
||||
Status string `json:"status"`
|
||||
// Kind is the library bucket: manga or novel. Empty on the way in means
|
||||
// "no opinion" — see Upsert.
|
||||
Kind string `json:"kind"`
|
||||
}
|
||||
|
||||
// HasNewChapter reports whether the site has published past the read point.
|
||||
@@ -96,6 +99,14 @@ func (b Bookmark) Initial() string {
|
||||
return "?"
|
||||
}
|
||||
|
||||
// Library buckets. A bookmark is in exactly one. This cannot be derived from
|
||||
// Site: asurascans serves manga and novels from the same /comics/ path, so the
|
||||
// userscript that recorded the page is the only party that knows which.
|
||||
const (
|
||||
KindManga = "manga"
|
||||
KindNovel = "novel"
|
||||
)
|
||||
|
||||
// Lifecycle buckets. A bookmark is in exactly one; favorite is orthogonal.
|
||||
const (
|
||||
StatusReading = "reading"
|
||||
@@ -119,6 +130,7 @@ CREATE TABLE IF NOT EXISTS bookmarks (
|
||||
latest_chapter_num REAL,
|
||||
latest_checked_at INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'reading',
|
||||
kind TEXT NOT NULL DEFAULT 'manga',
|
||||
updated_at INTEGER NOT NULL
|
||||
);`
|
||||
|
||||
@@ -135,11 +147,14 @@ var addedColumns = []struct{ name, ddl string }{
|
||||
// Lifecycle bucket. The DEFAULT backfills every pre-existing row as
|
||||
// 'reading', so there is no separate migration step.
|
||||
{"status", `ALTER TABLE bookmarks ADD COLUMN status TEXT NOT NULL DEFAULT 'reading'`},
|
||||
// Library bucket. The DEFAULT backfills every pre-existing row as 'manga',
|
||||
// which is what every row written before novels existed actually is.
|
||||
{"kind", `ALTER TABLE bookmarks ADD COLUMN kind TEXT NOT NULL DEFAULT 'manga'`},
|
||||
}
|
||||
|
||||
const bookmarkColumns = `key, site, series_id, title, series_url, cover,
|
||||
last_chapter, last_chapter_num, last_chapter_url,
|
||||
favorite, latest_chapter, latest_chapter_num, updated_at, status`
|
||||
favorite, latest_chapter, latest_chapter_num, updated_at, status, kind`
|
||||
|
||||
// Store is the SQLite-backed bookmark store.
|
||||
type Store struct {
|
||||
@@ -296,7 +311,7 @@ func scanBookmark(scan func(...any) error) (Bookmark, error) {
|
||||
if err := scan(
|
||||
&b.Key, &b.Site, &b.SeriesID, &title, &seriesURL, &cover,
|
||||
&lastChapter, &lastChapterNum, &lastChapterURL,
|
||||
&favorite, &latestChapter, &latestChapterNum, &b.UpdatedAt, &status,
|
||||
&favorite, &latestChapter, &latestChapterNum, &b.UpdatedAt, &status, &b.Kind,
|
||||
); err != nil {
|
||||
return Bookmark{}, err
|
||||
}
|
||||
@@ -384,18 +399,20 @@ func (s *Store) Upsert(b Bookmark) (Bookmark, error) {
|
||||
// is the stored row and excluded.* is the incoming one; a brand-new key
|
||||
// never reaches this clause, so it keeps the fresh timestamp from VALUES.
|
||||
//
|
||||
// The status column resolves on the VALUES side, not in the conflict
|
||||
// clause: excluded.* is the row *after* these expressions are evaluated,
|
||||
// so a default applied there would look identical to a real 'reading' and
|
||||
// would overwrite an archived row on every PUT from a client that knows
|
||||
// nothing about the column. Resolved once here, an empty incoming status
|
||||
// means "keep what is stored", and only a brand-new row falls through to
|
||||
// the literal default. The subquery runs inside this transaction, so it
|
||||
// sees the row this statement is about to conflict with.
|
||||
// The status and kind columns resolve on the VALUES side, not in the
|
||||
// conflict clause: excluded.* is the row *after* these expressions are
|
||||
// evaluated, so a default applied there would look identical to a real
|
||||
// 'reading' / 'manga' and would overwrite an archived or novel row on
|
||||
// every PUT from a client that knows nothing about the column. Resolved
|
||||
// once here, an empty incoming status or kind means "keep what is
|
||||
// stored", and only a brand-new row falls through to the literal
|
||||
// default. The subquery runs inside this transaction, so it sees the
|
||||
// row this statement is about to conflict with.
|
||||
if _, err := tx.Exec(`
|
||||
INSERT INTO bookmarks (`+bookmarkColumns+`)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
COALESCE(NULLIF(?, ''), (SELECT status FROM bookmarks WHERE key = ?), 'reading'))
|
||||
COALESCE(NULLIF(?, ''), (SELECT status FROM bookmarks WHERE key = ?), 'reading'),
|
||||
COALESCE(NULLIF(?, ''), (SELECT kind FROM bookmarks WHERE key = ?), 'manga'))
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
site=excluded.site, series_id=excluded.series_id, title=excluded.title,
|
||||
series_url=excluded.series_url, cover=excluded.cover,
|
||||
@@ -405,6 +422,7 @@ func (s *Store) Upsert(b Bookmark) (Bookmark, error) {
|
||||
latest_chapter=excluded.latest_chapter,
|
||||
latest_chapter_num=excluded.latest_chapter_num,
|
||||
status=excluded.status,
|
||||
kind=excluded.kind,
|
||||
updated_at=CASE
|
||||
WHEN bookmarks.last_chapter_num IS NOT excluded.last_chapter_num
|
||||
THEN excluded.updated_at
|
||||
@@ -413,7 +431,8 @@ func (s *Store) Upsert(b Bookmark) (Bookmark, error) {
|
||||
b.Key, b.Site, b.SeriesID, b.Title, b.SeriesURL, b.Cover,
|
||||
b.LastChapter, b.LastChapterNum, b.LastChapterURL,
|
||||
b.Favorite, b.LatestChapter, latestNum, b.UpdatedAt,
|
||||
b.Status, b.Key); err != nil {
|
||||
b.Status, b.Key,
|
||||
b.Kind, b.Key); err != nil {
|
||||
return Bookmark{}, fmt.Errorf("upsert %q: %w", b.Key, err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user