feat: prevent adding duplicate library items

Block saving a new item when one already exists, so no duplicate is
created and existing progress is never overwritten. Matches by provider
id (external_source/external_id) when available, otherwise by media
type, year and title.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tronax 2026-06-15 02:15:36 +02:00
parent 3f07c78316
commit 767b0b50b0
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
3 changed files with 47 additions and 0 deletions

View file

@ -416,6 +416,32 @@ void Database::loadCustomFields(MediaItem &item) {
item.customFields.insert(q.value(0).toString(), q.value(1).toString());
}
std::optional<Database::DuplicateMatch> Database::findDuplicate(const MediaItem &item) {
QSqlDatabase db = QSqlDatabase::database(m_connName);
QSqlQuery q(db);
// Provider-backed entries carry a stable external id; match on that first.
if (!item.externalId.isEmpty() && !item.externalSource.isEmpty()) {
q.prepare(QStringLiteral(
"SELECT id,title FROM media WHERE external_source=? AND external_id=? "
"AND id<>? LIMIT 1"));
q.addBindValue(item.externalSource);
q.addBindValue(item.externalId);
q.addBindValue(item.id);
} else {
// Manual entries: same media type, year and title (case-insensitive).
q.prepare(QStringLiteral(
"SELECT id,title FROM media WHERE type=? AND year=? "
"AND LOWER(TRIM(title))=LOWER(?) AND id<>? LIMIT 1"));
q.addBindValue(mediaTypeToString(item.type));
q.addBindValue(item.year);
q.addBindValue(item.title.trimmed());
q.addBindValue(item.id);
}
if (!q.exec() || !q.next())
return std::nullopt;
return DuplicateMatch{ q.value(0).toInt(), q.value(1).toString() };
}
std::optional<MediaItem> Database::loadItem(int id) {
QSqlDatabase db = QSqlDatabase::database(m_connName);
QSqlQuery q(db);

View file

@ -43,6 +43,13 @@ public:
bool deleteItem(int id);
std::optional<MediaItem> loadItem(int id);
// Identifies an existing library item that a given item would duplicate.
struct DuplicateMatch { int id; QString title; };
// Returns such a match, or nullopt. The item's own id is excluded (so an
// item never matches itself). Matches by (external_source, external_id)
// when both are set, otherwise by (type, title, year).
std::optional<DuplicateMatch> findDuplicate(const MediaItem &item);
// Returns items (with full segment trees) matching the criteria.
QVector<MediaItem> queryItems(const FilterCriteria &c);

View file

@ -455,6 +455,20 @@ void EditDialog::accept()
}
m_item.segments = rebuilt;
// Block adding an item that already exists, so no duplicate is created and
// no existing progress gets overwritten. Only new items are checked; an
// edit keeps its own id and is therefore never flagged.
if (m_item.id < 0) {
if (auto dup = m_db->findDuplicate(m_item)) {
QMessageBox::warning(this, windowTitle(),
QStringLiteral("„%1\" ist bereits in deiner Bibliothek vorhanden.\n\n"
"Es wurde nichts hinzugefügt, damit dein bestehender "
"Fortschritt nicht überschrieben wird.")
.arg(dup->title));
return;
}
}
if (!m_item.dateAdded.isValid())
m_item.dateAdded = QDateTime::currentDateTime();