Merge manga favorite related fields into favorite_at

Migration 16 replaces favorite, date_added and favorite_modified_at with one nullable favorite_at. A favorite whose date_added is 0 falls back to favorite_modified_at. Manga.favorite and dateAdded become getters derived from it, and restore merges it by taking the earliest non-zero time.
This commit is contained in:
AntsyLich
2026-09-27 22:37:02 +06:00
parent f8fff318ba
commit 986f46c09a
18 changed files with 183 additions and 109 deletions
@@ -72,11 +72,11 @@ class RestoreRepositoryImpl(
private fun mergeManga(manga: Manga, dbManga: Manga): Manga {
val details = if (dbManga.initialized || !manga.initialized) dbManga else manga
return dbManga.copy(
favorite = dbManga.favorite || manga.favorite,
dateAdded = listOfNotNull(
dbManga.dateAdded.takeIf { it > 0 },
manga.dateAdded.takeIf { it > 0 },
).minOrNull() ?: 0L,
favoriteAt = if (dbManga.favorite || manga.favorite) {
listOfNotNull(dbManga.favoriteAt, manga.favoriteAt).filter { it > 0 }.minOrNull() ?: 0L
} else {
null
},
title = details.title,
artist = details.artist,
author = details.author,
@@ -93,6 +93,7 @@ class RestoreRepositoryImpl(
private suspend fun updateManga(manga: Manga): Manga {
database.mangasQueries.updateFromBackup(
favoriteAt = manga.favoriteAt,
artist = manga.artist,
author = manga.author,
description = manga.description,
@@ -100,13 +101,11 @@ class RestoreRepositoryImpl(
title = manga.title,
status = manga.status,
thumbnailUrl = manga.thumbnailUrl,
favorite = manga.favorite,
lastUpdate = manga.lastUpdate,
initialized = manga.initialized,
viewer = manga.viewerFlags,
chapterFlags = manga.chapterFlags,
coverLastModified = manga.coverLastModified,
dateAdded = manga.dateAdded,
mangaId = manga.id,
updateStrategy = manga.updateStrategy,
notes = manga.notes,
@@ -117,6 +116,7 @@ class RestoreRepositoryImpl(
private suspend fun insertManga(manga: Manga): Long {
return database.mangasQueries.insertReturningId(
favoriteAt = manga.favoriteAt,
source = manga.source,
url = manga.url,
artist = manga.artist,
@@ -126,7 +126,6 @@ class RestoreRepositoryImpl(
title = manga.title,
status = manga.status,
thumbnailUrl = manga.thumbnailUrl,
favorite = manga.favorite,
lastUpdate = manga.lastUpdate,
nextUpdate = 0L,
calculateInterval = 0L,
@@ -134,7 +133,6 @@ class RestoreRepositoryImpl(
viewerFlags = manga.viewerFlags,
chapterFlags = manga.chapterFlags,
coverLastModified = manga.coverLastModified,
dateAdded = manga.dateAdded,
updateStrategy = manga.updateStrategy,
notes = manga.notes,
memo = manga.memo,
@@ -18,27 +18,24 @@ object MangaMapper {
title: String,
status: Long,
thumbnailUrl: String?,
favorite: Boolean,
lastUpdate: Long?,
nextUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
chapterFlags: Long,
coverLastModified: Long,
dateAdded: Long,
updateStrategy: UpdateStrategy,
calculateInterval: Long,
favoriteModifiedAt: Long?,
notes: String,
memo: JsonObject,
favoriteAt: Long?,
): Manga = Manga(
id = id,
source = source,
favorite = favorite,
lastUpdate = lastUpdate ?: 0,
nextUpdate = nextUpdate ?: 0,
fetchInterval = calculateInterval.toInt(),
dateAdded = dateAdded,
favoriteAt = favoriteAt,
viewerFlags = viewerFlags,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
@@ -52,7 +49,6 @@ object MangaMapper {
thumbnailUrl = thumbnailUrl,
updateStrategy = updateStrategy,
initialized = initialized,
favoriteModifiedAt = favoriteModifiedAt,
notes = notes,
memo = memo,
)
@@ -68,19 +64,17 @@ object MangaMapper {
title: String,
status: Long,
thumbnailUrl: String?,
favorite: Boolean,
lastUpdate: Long?,
nextUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
chapterFlags: Long,
coverLastModified: Long,
dateAdded: Long,
updateStrategy: UpdateStrategy,
calculateInterval: Long,
favoriteModifiedAt: Long?,
notes: String,
memo: JsonObject,
favoriteAt: Long?,
totalCount: Long,
readCount: Double,
latestUpload: Long,
@@ -100,19 +94,17 @@ object MangaMapper {
title,
status,
thumbnailUrl,
favorite,
lastUpdate,
nextUpdate,
initialized,
viewerFlags,
chapterFlags,
coverLastModified,
dateAdded,
updateStrategy,
calculateInterval,
favoriteModifiedAt,
notes,
memo,
favoriteAt,
),
categories = categories.split(",").map { it.toLong() },
totalChapters = totalCount,
@@ -134,19 +126,17 @@ object MangaMapper {
title: String,
status: Long,
thumbnailUrl: String?,
favorite: Boolean,
lastUpdate: Long?,
nextUpdate: Long?,
initialized: Boolean,
viewerFlags: Long,
chapterFlags: Long,
coverLastModified: Long,
dateAdded: Long,
updateStrategy: UpdateStrategy,
calculateInterval: Long,
favoriteModifiedAt: Long?,
notes: String,
memo: JsonObject,
favoriteAt: Long?,
totalCount: Long,
): MangaWithChapterCount = MangaWithChapterCount(
manga = mapManga(
@@ -160,19 +150,17 @@ object MangaMapper {
title,
status,
thumbnailUrl,
favorite,
lastUpdate,
nextUpdate,
initialized,
viewerFlags,
chapterFlags,
coverLastModified,
dateAdded,
updateStrategy,
calculateInterval,
favoriteModifiedAt,
notes,
memo,
favoriteAt,
),
chapterCount = totalCount,
)
@@ -198,7 +198,7 @@ class MangaRepositoryImpl(
title = it.title,
status = it.status,
thumbnailUrl = it.thumbnailUrl,
favorite = it.favorite,
favoriteAt = it.favoriteAt,
lastUpdate = it.lastUpdate,
nextUpdate = it.nextUpdate,
calculateInterval = it.fetchInterval.toLong(),
@@ -206,7 +206,6 @@ class MangaRepositoryImpl(
viewerFlags = it.viewerFlags,
chapterFlags = it.chapterFlags,
coverLastModified = it.coverLastModified,
dateAdded = it.dateAdded,
updateStrategy = it.updateStrategy,
memo = it.memo,
updateTitle = it.title.isNotBlank(),
@@ -247,14 +246,14 @@ class MangaRepositoryImpl(
mangaUpdates.forEach { value ->
with(value) {
database.mangasQueries.update(
favorite = favorite,
favoriteAtSet = isSet(::favoriteAt),
favoriteAt = favoriteAt,
lastUpdate = lastUpdate,
nextUpdate = nextUpdate,
calculateInterval = fetchInterval?.toLong(),
viewer = viewerFlags,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
dateAdded = dateAdded,
mangaId = id,
notes = notes,
)
@@ -15,33 +15,23 @@ CREATE TABLE mangas(
title TEXT NOT NULL,
status INTEGER NOT NULL,
thumbnail_url TEXT,
favorite INTEGER AS Boolean NOT NULL,
last_update INTEGER,
next_update INTEGER,
initialized INTEGER AS Boolean NOT NULL,
viewer INTEGER NOT NULL,
chapter_flags INTEGER NOT NULL,
cover_last_modified INTEGER NOT NULL,
date_added INTEGER NOT NULL,
update_strategy INTEGER AS UpdateStrategy NOT NULL DEFAULT 0,
calculate_interval INTEGER DEFAULT 0 NOT NULL,
favorite_modified_at INTEGER,
notes TEXT NOT NULL DEFAULT "",
memo BLOB AS JsonObject NOT NULL DEFAULT '{}'
memo BLOB AS JsonObject NOT NULL DEFAULT '{}',
favorite_at INTEGER
);
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
CREATE INDEX library_favorite_at_index ON mangas(favorite_at) WHERE favorite_at IS NOT NULL;
CREATE INDEX mangas_url_index ON mangas(url);
CREATE INDEX idx_mangas_source ON mangas(source);
CREATE TRIGGER update_last_favorited_at_mangas
AFTER UPDATE OF favorite ON mangas
BEGIN
UPDATE mangas
SET favorite_modified_at = strftime('%s', 'now')
WHERE _id = new._id;
END;
getMangaById:
SELECT *
FROM mangas
@@ -58,12 +48,12 @@ LIMIT 1;
getFavorites:
SELECT *
FROM mangas
WHERE favorite = 1;
WHERE favorite_at IS NOT NULL;
getReadMangaNotInLibrary:
SELECT *
FROM mangas
WHERE favorite = 0 AND _id IN (
WHERE favorite_at IS NULL AND _id IN (
SELECT DISTINCT chapters.manga_id
FROM chapters
WHERE read = 1 OR last_page_read != 0
@@ -77,23 +67,18 @@ getAllMangaSourceAndUrl:
SELECT source, url
FROM mangas;
getMangasWithFavoriteTimestamp:
SELECT *
FROM mangas
WHERE favorite_modified_at IS NOT NULL;
getSourceIdWithFavoriteCount:
SELECT
source,
count(*)
FROM mangas
WHERE favorite = 1
WHERE favorite_at IS NOT NULL
GROUP BY source;
getFavoriteBySourceId:
SELECT *
FROM mangas
WHERE favorite = 1
WHERE favorite_at IS NOT NULL
AND source = :sourceId;
getDuplicateLibraryManga:
@@ -113,7 +98,7 @@ duplicates AS (
FROM mangas M
LEFT JOIN track_dupes D
ON D.manga_id = _id
WHERE favorite = 1
WHERE favorite_at IS NOT NULL
AND _id != :id
AND (lower(title) LIKE '%' || lower(:title) || '%' OR D.manga_id IS NOT NULL)
),
@@ -141,7 +126,7 @@ getUpcomingManga:
SELECT *
FROM mangas
WHERE next_update >= :startOfDay
AND favorite = 1
AND favorite_at IS NOT NULL
AND status IN :statuses
AND (
-- includedEmpty being true expresses "don't care" state and bypasses the membership filter
@@ -179,12 +164,12 @@ SET viewer = 0;
getSourceIdsWithNonLibraryManga:
SELECT source, COUNT(*) AS manga_count
FROM mangas
WHERE favorite = 0
WHERE favorite_at IS NULL
GROUP BY source;
deleteNonLibraryManga:
DELETE FROM mangas
WHERE favorite = 0
WHERE favorite_at IS NULL
AND source IN :sourceIds
AND (
:keepReadManga = 0
@@ -197,19 +182,19 @@ AND (
);
insertReturningId:
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, notes, memo)
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, :notes, :memo)
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite_at, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, update_strategy, calculate_interval, notes, memo)
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favoriteAt, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :updateStrategy, :calculateInterval, :notes, :memo)
RETURNING _id;
update:
UPDATE mangas SET
favorite = coalesce(:favorite, favorite),
-- NULL is a real value here (not in the library), which coalesce cannot write
favorite_at = CASE WHEN :favoriteAtSet THEN :favoriteAt ELSE favorite_at END,
last_update = coalesce(:lastUpdate, last_update),
next_update = coalesce(:nextUpdate, next_update),
viewer = coalesce(:viewer, viewer),
chapter_flags = coalesce(:chapterFlags, chapter_flags),
cover_last_modified = coalesce(:coverLastModified, cover_last_modified),
date_added = coalesce(:dateAdded, date_added),
calculate_interval = coalesce(:calculateInterval, calculate_interval),
notes = coalesce(:notes, notes)
WHERE _id = :mangaId;
@@ -223,13 +208,12 @@ UPDATE mangas SET
title = coalesce(:title, title),
status = coalesce(:status, status),
thumbnail_url = coalesce(:thumbnailUrl, thumbnail_url),
favorite = coalesce(:favorite, favorite),
favorite_at = :favoriteAt,
last_update = coalesce(:lastUpdate, last_update),
initialized = coalesce(:initialized, initialized),
viewer = coalesce(:viewer, viewer),
chapter_flags = coalesce(:chapterFlags, chapter_flags),
cover_last_modified = coalesce(:coverLastModified, cover_last_modified),
date_added = coalesce(:dateAdded, date_added),
update_strategy = coalesce(:updateStrategy, update_strategy),
notes = coalesce(:notes, notes),
memo = coalesce(:memo, memo)
@@ -253,13 +237,13 @@ WHERE _id = :mangaId;
insertNetworkManga {
-- Insert the manga if it doesn't exist already
INSERT INTO mangas(
source, url, artist, author, description, genre, title, status, thumbnail_url, favorite,
last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added,
source, url, artist, author, description, genre, title, status, thumbnail_url, favorite_at,
last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified,
update_strategy, calculate_interval, memo
)
SELECT
:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite,
:lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded,
:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favoriteAt,
:lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified,
:updateStrategy, :calculateInterval, :memo
WHERE NOT EXISTS(SELECT 0 FROM mangas WHERE source = :source AND url = :url);
@@ -278,7 +262,7 @@ insertNetworkManga {
memo = :memo
WHERE source = :source
AND url = :url
AND favorite = 0;
AND favorite_at IS NULL;
-- Finally return the manga
SELECT *
@@ -0,0 +1,113 @@
-- Merge favorite, date_added and favorite_modified_at into favorite_at
DROP VIEW historyView;
DROP VIEW libraryView;
DROP VIEW updatesView;
DROP TRIGGER update_last_favorited_at_mangas;
DROP INDEX library_favorite_index;
ALTER TABLE mangas ADD COLUMN favorite_at INTEGER;
UPDATE mangas
SET favorite_at = coalesce(nullif(date_added, 0), favorite_modified_at * 1000, 0)
WHERE favorite = 1;
ALTER TABLE mangas DROP COLUMN favorite;
ALTER TABLE mangas DROP COLUMN date_added;
ALTER TABLE mangas DROP COLUMN favorite_modified_at;
CREATE INDEX library_favorite_at_index ON mangas(favorite_at) WHERE favorite_at IS NOT NULL;
CREATE VIEW libraryView AS
SELECT
M.*,
coalesce(C.total, 0) AS totalCount,
coalesce(C.readCount, 0) AS readCount,
coalesce(C.latestUpload, 0) AS latestUpload,
coalesce(C.fetchedAt, 0) AS chapterFetchedAt,
coalesce(C.lastRead, 0) AS lastRead,
coalesce(C.bookmarkCount, 0) AS bookmarkCount,
coalesce(MC.categories, '0') AS categories
FROM mangas M
LEFT JOIN (
SELECT
chapters.manga_id,
count(*) AS total,
sum(read) AS readCount,
coalesce(max(chapters.date_upload), 0) AS latestUpload,
coalesce(max(history.last_read), 0) AS lastRead,
coalesce(max(chapters.date_fetch), 0) AS fetchedAt,
sum(chapters.bookmark) AS bookmarkCount
FROM chapters
LEFT JOIN excluded_scanlators
ON chapters.manga_id = excluded_scanlators.manga_id
AND chapters.scanlator = excluded_scanlators.scanlator
LEFT JOIN history
ON chapters._id = history.chapter_id
WHERE excluded_scanlators.scanlator IS NULL
GROUP BY chapters.manga_id
) AS C
ON M._id = C.manga_id
LEFT JOIN (
SELECT manga_id, group_concat(category_id) AS categories
FROM mangas_categories
GROUP BY manga_id
) AS MC
ON MC.manga_id = M._id
WHERE M.favorite_at IS NOT NULL;
CREATE VIEW updatesView AS
SELECT
mangas._id AS mangaId,
mangas.title AS mangaTitle,
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.url AS chapterUrl,
chapters.read,
chapters.bookmark,
chapters.last_page_read,
mangas.source,
mangas.favorite_at IS NOT NULL AS favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch,
excluded_scanlators.scanlator AS excludedScanlator
FROM mangas JOIN chapters
ON mangas._id = chapters.manga_id
LEFT JOIN excluded_scanlators
ON mangas._id = excluded_scanlators.manga_id
AND chapters.scanlator = excluded_scanlators.scanlator
WHERE favorite_at IS NOT NULL
AND date_fetch > favorite_at
ORDER BY date_fetch DESC;
CREATE VIEW historyView AS
SELECT
history._id AS id,
mangas._id AS mangaId,
chapters._id AS chapterId,
mangas.title,
mangas.thumbnail_url AS thumbnailUrl,
mangas.source,
mangas.favorite_at IS NOT NULL AS favorite,
mangas.cover_last_modified,
chapters.chapter_number AS chapterNumber,
history.last_read AS readAt,
history.time_read AS readDuration,
max_last_read.last_read AS maxReadAt,
max_last_read.chapter_id AS maxReadAtChapterId
FROM mangas
JOIN chapters
ON mangas._id = chapters.manga_id
JOIN history
ON chapters._id = history.chapter_id
JOIN (
SELECT chapters.manga_id,chapters._id AS chapter_id, MAX(history.last_read) AS last_read
FROM chapters JOIN history
ON chapters._id = history.chapter_id
GROUP BY chapters.manga_id
) AS max_last_read
ON chapters.manga_id = max_last_read.manga_id;
@@ -6,7 +6,7 @@ SELECT
mangas.title,
mangas.thumbnail_url AS thumbnailUrl,
mangas.source,
mangas.favorite,
mangas.favorite_at IS NOT NULL AS favorite,
mangas.cover_last_modified,
chapters.chapter_number AS chapterNumber,
history.last_read AS readAt,
@@ -34,7 +34,7 @@ LEFT JOIN (
GROUP BY manga_id
) AS MC
ON MC.manga_id = M._id
WHERE M.favorite = 1;
WHERE M.favorite_at IS NOT NULL;
library:
SELECT *
@@ -10,7 +10,7 @@ SELECT
chapters.bookmark,
chapters.last_page_read,
mangas.source,
mangas.favorite,
mangas.favorite_at IS NOT NULL AS favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload,
@@ -21,8 +21,8 @@ ON mangas._id = chapters.manga_id
LEFT JOIN excluded_scanlators
ON mangas._id = excluded_scanlators.manga_id
AND chapters.scanlator = excluded_scanlators.scanlator
WHERE favorite = 1
AND date_fetch > date_added
WHERE favorite_at IS NOT NULL
AND date_fetch > favorite_at
ORDER BY date_fetch DESC;
getRecentUpdates: