Commit Graph

50 Commits

Author SHA1 Message Date
389d177d85 fix: address code review findings
- Fix duplicate Telegram notifications: SyncArtistDiscography no longer wipes
  notifications_sent for the whole artist on every cache-miss re-sync; only
  markers for releases that disappear are pruned (FK-safe via INSERT OR REPLACE
  + rgid NOT IN (...)).
- Cache empty MusicBrainz discographies via a new artist_settings.last_synced
  column (migration 009) so zero-release artists honor the TTL instead of being
  re-fetched every cycle.
- Wire the Web UI server and Telegram notifier scheduler into main.run/NewApp.
- Guard startPeriodicSync against overlapping syncs with a done-channel slot.
- Add server.public_url config; NewServerWithConfig derives reachable links
  and no longer advertises the 0.0.0.0 bind address.
- Web handlers: use scanner.ScanArtist per artist, drop always-false
  releaseIgnored lookup and dead endsWith, thread configured threshold.
- Limit :memory: DB pool to one connection so migrations and queries share the
  same in-memory store.
2026-07-19 23:38:33 +03:00
e493a4d228 feat: document wired sync pipeline, notifier, and web UI in README 2026-07-19 22:50:39 +03:00
1a644dede7 feat: verify acceptance criteria and document sync config
Add a scan->notify->web end-to-end smoke test against a seeded :memory: DB,
fix .gitignore so the cmd/naviwatcher source dir is not excluded by the
root binary rule, and document the new sync.interval field in config.yaml.example.
2026-07-19 22:48:31 +03:00
44f3b0a2a7 feat: add Web UI artist detail, archive, and ignore actions
Implements Task 8: artist detail page (local albums + found-missing
with ignore buttons), ignored-releases archive with restore, and POST
handlers toggling ignore flags and ignore_singles. Adds ErrArtistNotFound
sentinel so callers can distinguish missing artists, and wires routes via
the enhanced ServeMux path wildcard.
2026-07-19 22:43:47 +03:00
cea20957e7 feat: add Web UI server with basic-auth and artist dashboard 2026-07-19 22:35:28 +03:00
11f838ace9 feat: add notifier scheduler and NotifyOnce sent-tracking 2026-07-19 22:32:57 +03:00
3af33bd728 feat: add notifier Sender interface and digest formatter 2026-07-19 22:28:41 +03:00
0635ca8a87 feat: wire main loop with periodic sync+scan and sync_interval config
Replace compute-only run() with an immediate sync+scan followed by a
ticker-driven periodic loop, add the sync.interval config field (default
6h) with defaults and validation, and add tests for the scheduling logic.
2026-07-19 22:27:06 +03:00
dc4bdcdab0 feat: add SyncAll periodic sync pipeline and wire Navidrome client into App 2026-07-19 22:22:46 +03:00
40c4240693 feat: add MusicBrainz artist-ID resolver
Add ResolveArtistMBID to query the MB artist search endpoint and return the
first matching artist ID, with httptest-backed tests for match, no-match,
HTTP error, and invalid-JSON paths.
2026-07-19 22:16:43 +03:00
85c42ec858 feat: add mbid column to artist_settings
Additive migration 008 adds an mbid column to artist_settings, extend the
ArtistSettings struct and persistence functions (SaveArtistSettings,
GetArtistSettings, GetAllArtistSettings, UpdateArtistSettings) to carry the
MusicBrainz ID, and add round-trip tests covering empty and set MBID values.
2026-07-19 22:13:57 +03:00
35d6a5234f move completed plan: 2026-07-19-scanner-engine-fuzzy-diff.md 2026-07-19 21:59:59 +03:00
79a376a127 fix: address code review findings
- Strip standalone reissue keywords (remaster/remastered/remix/deluxe/
  expanded/edition/reissue/anniversary/bonus) regardless of brackets so
  non-parenthesized remasters still match the plain local title above the
  0.85 threshold (was falsely reported missing).
- Fix TestAppRun_ScanLogsMissingReleases to verify run() performs the
  scan itself (capture its log output) instead of re-running ScanAll
  independently, which passed even if run() were a no-op.
- Fix TestSimilarity misleading cases that downgraded to a <0.85 range
  check with dead want/epsilon fields; assert actual computed scores.
2026-07-19 21:33:52 +03:00
e2de91c5d3 fix: address code review findings
- Fix pagination to not trust MusicBrainz count for cutoff (prevents
  silent discography truncation / hidden missing releases)
- Add idx_external_releases_artist_id index for hot per-artist queries
- Dedupe cached_at layout constant in external_releases.go
2026-07-19 21:13:24 +03:00
ab2af62cb0 fix: guard against whitespace-only type attribute panic in ParseReleaseGroups 2026-07-19 21:08:57 +03:00
2468859435 fix: address code review findings 2026-07-19 20:53:06 +03:00
da8b8aa944 fix: address code review findings (consolidate slice helpers, correct bootleg comment) 2026-07-19 20:10:06 +03:00
4da5ee5f8c fix: address code review findings 2026-07-19 20:06:41 +03:00
b21bf07208 fix: address code review findings
- Store cached_at in canonical UTC layout so the cache TTL cutoff comparison is a valid time ordering (previously go-sqlite3 serialized time.Time as RFC3339, making the space-separated cutoff match only by ASCII accident; same-day expired entries were falsely served as fresh).

- Remove stale no-op scanner config keys (ignore_bootlegs, include_compilations) from config.yaml.example and docs; these fields were removed from ScannerConfig but left in configs, silently doing nothing.
2026-07-19 20:01:26 +03:00
0bee9b9b27 fix: address code review findings 2026-07-19 19:52:27 +03:00
a7803615cd fix: address code review findings 2026-07-19 19:45:08 +03:00
beef81d598 fix: address code review findings 2026-07-19 19:28:51 +03:00
401c1218b6 fix: address code review findings 2026-07-19 19:22:18 +03:00
f1839ad9e7 fix: address code review findings 2026-07-19 19:11:22 +03:00
c70f46af27 fix: address code review findings 2026-07-19 19:07:20 +03:00
a4c426f640 fix: address code review findings 2026-07-19 18:59:38 +03:00
8a5b58a817 fix: address code review findings
- Fix artist-ID namespace mismatch in MusicBrainz provider: SyncArtistDiscography
  now stores the canonical Navidrome artist ID (artist_settings.id) as
  external_releases.artist_id instead of the MusicBrainz MBID. Previously the
  MBID was stored, which violated the FK to artist_settings and broke the
  scanner join (local_albums.artist_id is the Navidrome ID), causing every
  external release to be falsely reported as missing and the sync insert to
  fail at runtime. getArtistFilterOptions now also resolves by the Navidrome ID.
- Resolve threshold in FindMissingReleases so the exported primitive honors the
  same zero-means-default contract as ScanArtist/ScanAll.
- Remove dead maxLen==0 guard in scanner.Similarity.
- Inline trivial buildPath helper; drop unused url import in client.go.
- Replace hand-rolled itoa with strconv.Itoa in tests.
- Rewrite SyncArtistDiscography tests to seed artist_settings with the Navidrome
  ID (tests previously seeded the MBID to mask the FK mismatch).
- Fix TestFuzzySmoke to exercise the real dependency (fuzzy.LevenshteinDistance /
  scanner.Similarity) instead of an unused API.
- Fix TestAppRun_ScanLogsMissingReleases to run the scan against a live context
  and assert the missing release is found.
- Document cached_at column in Specification.md and note startup scan / required
  musicbrainz.user_agent in README.
- Stop tracking .serena/ tooling config; add it to .gitignore.
2026-07-19 18:41:18 +03:00
06e09220ae feat: document Scanner Engine implementation status and normalize convention 2026-07-19 18:23:13 +03:00
9fc89fd5a4 feat: verify scanner engine acceptance criteria
Confirm normalization + 0.85 threshold, IsIgnored skip, per-artist scoping,
and zero-threshold→default contract. Added TestScanArtist_ZeroThresholdUsesDefault,
fixed gofmt on two test files. Coverage: scanner 90.3%, normalize 100.0%.
2026-07-19 18:21:51 +03:00
c2da8c2095 feat: add DB-backed scanner entrypoint and compute-only main hook
Implements ScanArtist/ScanAll in internal/scanner loading local/external
releases via the database layer with ctx-cancellation checks, plus a
compute-only run() hook that logs missing-release counts. Add table-driven
tests using in-memory SQLite fixtures.
2026-07-19 18:19:57 +03:00
a4f0460664 feat: implement scanner diff engine (missing-release detection)
Add FindMissingReleases and MissingRelease to internal/scanner: skips
IsIgnored external releases, scopes matches per ArtistID, and reports
external releases with no local album above the fuzzy threshold.
2026-07-19 18:16:58 +03:00
92aafaab05 feat: implement scanner similarity scoring 2026-07-19 18:14:49 +03:00
edf9c1d1f8 feat: extract normalization into internal/normalize package 2026-07-19 18:11:22 +03:00
c95c740cd5 feat: add fuzzysearch dependency for scanner engine 2026-07-19 18:09:41 +03:00
424be1efc4 fix: address fourth code review findings
- Fix FK constraint violation in SyncArtistDiscography: delete
  notifications_sent rows before external_releases to prevent
  constraint failure when re-syncing artists with prior notifications.
- Implement per-artist type filtering: FilterReleaseGroups now accepts
  FilterOptions with IgnoreSingles/IgnoreCompilations flags, read from
  artist_settings table via getArtistFilterOptions.
- Fix inconsistent error wrapping: GetExternalRelease now wraps errors
  with fmt.Errorf like all other functions in the package; updated test
  to use errors.Is for sql.ErrNoRows check.
- Add tests: FilterReleaseGroups ignore singles/compilations,
  SyncArtistDiscography per-artist type filtering, and FK-safe resync.
2026-05-26 17:29:05 +03:00
5d52a09868 fix: address third code review findings
- Prevent infinite pagination loop when API returns empty release groups page
- Move NormalizeString regexes to package level to avoid recompilation on every call
- Use UTC consistently for cached_at timestamps to avoid DST-related TTL skew
2026-05-26 16:24:55 +03:00
582202bb86 fix: address second code review findings
- Add context cancellation check between pagination pages in GetArtistReleaseGroups
  for responsive graceful shutdown during large discography fetches.
- Eliminate double DB query on cache hit by having GetCachedReleases return
  []ExternalRelease directly instead of just a count, avoiding a redundant
  second query in SyncArtistDiscography.
- Update cache_test.go to match new GetCachedReleases return type.
- Format main.go (pre-existing whitespace issue).
2026-05-26 15:58:40 +03:00
2baf586607 fix: address second code review findings
- Add CacheTTL default (24h) in config applyDefaults — without this,
  omitting cache_ttl from config silently defeats the entire caching
  mechanism (TTL=0 means cached data is never served)
- Fix pagination to use total Count instead of checking if last page
  was short — avoids wasting a rate-limit token when total count is
  an exact multiple of 100
- Preserve user-set IsIgnored flags across re-syncs — previously,
  DELETE+INSERT in the sync transaction reset all ignore flags to
  false, losing user preferences on every cache-expiry re-sync
- Check context cancellation on cache-hit code path — previously,
  ctx.Err() was not checked between cache check and returning cached
  data, violating the cancellation contract
2026-05-26 14:55:50 +03:00
a5911c257c fix: address code review findings
Remove dead code: duplicate ExternalRelease/Artist/ParsedArtist structs in
model.go, ParseArtist/mbArtist/mbArtistData in client.go, ArtistTypeFilter
and related filtering functions in api.go, SyncArtistDiscographyWithFilter
in sync.go, and CacheStats/IsArtistCacheValid in cache.go.

Fix bugs: SaveExternalRelease now stores NULL instead of empty string for
zero CachedAt; sync upserts are now transactional with stale release cleanup;
getCachedReleases returns int instead of *CacheStats; doGet uses url.Values
for proper query encoding of MBID.

Fix tests: removed duplicate TestRun_GracefulShutdown, removed dead code
(_ = dbPath) from TestNewApp, fixed assertions in httptest handler goroutine
to avoid data race, increased rate limiter timing tolerance, removed
Client.Close() calls (no-op removed), fixed sync test cache expiry to use
UPDATE instead of 0 TTL races.

Fix formatting: cancel()}() formatting in main.go, error format string in sync.go.
2026-05-26 14:10:22 +03:00
34ea84fc77 feat: verify MusicBrainz provider acceptance criteria
- All 6 tasks complete: client/models, rate limiting/caching, API
  endpoints/filtering, sync orchestration, app wiring, verification
- Full test suite passes (all packages green)
- go vet clean, no issues
- Coverage: musicbrainz 92.5%, config 100%, navidrome 79.5%, database 53.1%
- Edge cases verified: HTTP errors, rate limit blocking, context
  cancellation, NULL handling, XML parse errors
2026-05-26 13:17:49 +03:00
da49d12bb6 feat: wire up MusicBrainz provider in application entry point
- Add App struct with Config, DB, and MusicBrainz client dependencies
- NewApp() initializes database and MusicBrainz client from config
- App.Close() cleans up resources (MB client + DB) on shutdown
- App.run() blocks until context cancelled (goroutine-ready for future tasks)
- Config validation already requires musicbrainz.user_agent
- Add 6 tests: graceful shutdown, config integration, NewApp creation,
  shutdown cleanup, app.run shutdown, UserAgent validation
- All tests pass (6/6 in cmd, full suite green)
2026-05-26 13:14:47 +03:00
15b05b57fb feat: implement MusicBrainz sync orchestration with caching and upsert
Creates internal/musicbrainz/sync.go with SyncArtistDiscography function
that orchestrates the full flow: cache check via GetCachedReleases, fetch
from MusicBrainz API on cache miss, filter via FilterReleaseGroups, and
upsert into external_releases via database.SaveExternalRelease. Includes
SyncArtistDiscographyWithFilter variant for per-artist type filtering. All
functions support context cancellation. 14 new tests cover cache hit/miss,
status/type filtering, context cancellation, idempotency, API errors, and
full XML pipeline integration.
2026-05-26 13:04:20 +03:00
e624bb0eaf feat: implement MusicBrainz API endpoints, filtering, and normalization
- Add GetArtistReleaseGroups method with pagination support
- Implement status filtering (exclude Bootleg/Promotion/Pseudo-Release)
- Implement type filtering (include Album/Single/EP/Compilation)
- Add per-artist type filtering hooks (ArtistTypeFilter) for Web UI
- Add NormalizeString and NormalizeArtistName for fuzzy matching prep
- Add ReleaseGroup.ToExternalRelease conversion method
- Write comprehensive table-driven tests for filtering logic
- Write tests for normalization functions (18 cases)
- Write tests for GetArtistReleaseGroups (success, pagination, empty, errors)
- All tests pass (47 total across project), go vet clean
2026-05-26 12:31:07 +03:00
b0f69d3a4f feat: implement rate limiting and caching layer for MusicBrainz provider
- Add golang.org/x/time/rate dependency for token-bucket rate limiting
- Replace custom channel-based rate limiter with rate.NewLimiter(1, 1)
- Add context.Context support to doGet for cancellation
- Add cached_at column to external_releases via migration 005
- Implement cache hit/miss queries with TTL-based filtering
- Add CacheStats type for tracking cached RGIDs
- Update ExternalRelease struct with CachedAt field
- Add rate limiting tests (1 req/sec enforcement, burst behavior)
- Add cache tests (hit, miss, expired, mixed, empty artist)
- Update migration count test for new migration
2026-05-26 12:15:28 +03:00
674daed93b feat: create MusicBrainz client and data models
Add internal/musicbrainz/ package with:
- client.go: MusicBrainzClient struct wrapping net/http.Client with
  channel-based rate limiter (1 req/sec), doGet method with proper
  User-Agent header, and Close for cleanup
- model.go: ReleaseGroup, Artist, ExternalRelease, and Parsed* structs
- XML parsing functions for release-group list and artist responses
- Comprehensive tests: XML parsing (success, empty, malformed),
  client constructor, doGet (success, non-200, unreachable server),
  rate limiter behavior
2026-05-26 11:52:17 +03:00
60ecc3f904 Add MusicBrainz provider plan and ignore coverage output 2026-05-26 11:33:30 +03:00
a31b4c4fe1 Merge pull request 'feat: add Navidrome client with Subsonic API sync' (#1) from navidrome-client into master
Reviewed-on: #1
2026-05-21 06:50:15 +00:00
0065057514 feat: add Navidrome client with Subsonic API sync
Add the Navidrome client module that connects to a Navidrome server via
the Subsonic API, fetches artist and album data, and syncs it into the
local SQLite database.

- Add go-subsonic dependency for Subsonic API communication
- Create internal/navidrome/client.go with NavidromeClient wrapper
  - NewClient constructor with token-based auth
  - Ping health check with HTTP status validation
  - GetArtists fetches all artists via getArtists endpoint
  - GetArtistAlbums fetches albums per artist via getArtist endpoint
- Create internal/navidrome/sync.go with sync orchestration
  - SyncArtists upserts artists into artist_settings table
  - SyncAlbums fetches and stores albums for monitored artists
- Add local_albums table (migration 003) with FK to artist_settings
- Add LocalAlbum CRUD operations in internal/database/local_albums.go
- Full test coverage: 19 tests across client and sync packages
- All tests pass, go vet and go fmt clean
2026-05-21 09:45:27 +03:00
735ff0828e feat: add foundation layer (Go module, config, database, Docker)
Squashed commits from foundation-layer branch:

- Initialize Go module and project skeleton (cmd/naviwatcher/main.go)
- Add configuration management with YAML parsing and validation
- Add database layer with schema migrations (artist_settings, external_releases, notifications_sent)
- Add CRUD operations for artist_settings, external_releases, notifications_sent
- Add Docker setup with multi-stage build and docker-compose
- Verify acceptance criteria (tests, vet, fmt)
- Update README.md with build/run/test instructions
- Fix: filter ignored releases in GetUnnotifiedReleases (spec compliance)
- Fix: add FK constraint on notifications_sent.rgid
- Fix: add config.yaml to .gitignore (security)
- Fix: run Docker container as non-root user
- Fix: pin alpine:3.21 instead of alpine:latest
- Fix: wrap migrations in transactions for atomicity

All 49 tests pass, go vet clean, Docker image builds successfully.
2026-05-20 16:11:11 +03:00
001f9ce691 Initial commit 2026-05-19 17:42:12 +03:00