feat: add item notes and duplicate detection across platforms

- Add per-item notes editable in the detail and edit screens, synced
  between desktop and Android
- Guard against duplicates by external ID and by type/year/title,
  mirroring the desktop findDuplicate logic in MediaDao
- Wrap multi-step database operations in Room transactions
- Disable Android auto backup and remove the destructive Room
  migration fallback so schema bumps fail loudly instead of wiping data
- Bound crypto envelope KDF parameters when reading untrusted headers
- Handle malformed server responses in SyncClient instead of crashing
- Extend settings, database, and image cache on the desktop side
This commit is contained in:
Tronax 2026-08-16 12:04:28 +02:00
parent 85f5c6dd4e
commit 2b35139364
30 changed files with 837 additions and 124 deletions

48
server/ratelimit.go Normal file
View file

@ -0,0 +1,48 @@
package main
import (
"sync"
"time"
)
// rateLimiter is a minimal in-memory fixed-window limiter keyed by string
// (bearer-token subject for the TMDB proxy). It blunts runaway clients abusing
// the shared API key; it is not a full DoS defense.
type rateLimiter struct {
mu sync.Mutex
limit int
window time.Duration
counts map[string]rateEntry
}
type rateEntry struct {
windowStart time.Time
count int
}
func newRateLimiter(limit int, per time.Duration) *rateLimiter {
return &rateLimiter{limit: limit, window: per, counts: map[string]rateEntry{}}
}
func (r *rateLimiter) allow(key string) bool {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
e, ok := r.counts[key]
if !ok || now.Sub(e.windowStart) >= r.window {
// Occasionally garbage-collect stale keys so the map stays bounded.
if len(r.counts) > 10_000 {
for k, v := range r.counts {
if now.Sub(v.windowStart) >= r.window {
delete(r.counts, k)
}
}
}
r.counts[key] = rateEntry{windowStart: now, count: 1}
return true
}
e.count++
r.counts[key] = e
return e.count <= r.limit
}