diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index b9a1e1a..0696345 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -1,4 +1,4 @@ -name: Build and Push Docker Image +name: CI/CD on: push: @@ -8,70 +8,89 @@ on: branches: [ main, master ] env: - # Docker image configuration - using your Gitea registry + GO_VERSION: "1.25" REGISTRY: gitea.mrixs.me IMAGE_NAME: naviwatcher jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: go-test-${{ runner.os }}-${{ hashFiles('go.sum') }} + restore-keys: | + go-test-${{ runner.os }}- + + - name: Download Go modules + run: go mod download + + - name: Run tests + run: go test ./... -v -race -cover + build: runs-on: ubuntu-latest - permissions: - contents: read - packages: write # Needed for writing to GitHub Packages registry - + needs: test steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: - go-version: '1.25' - cache: true + go-version: ${{ env.GO_VERSION }} - - name: Verify dependencies - run: | - go mod tidy - go mod verify - - - name: Run unit tests - run: go test ./... -v -coverprofile=coverage.out - - - name: Upload coverage report - if: always() - uses: actions/upload-artifact@v4 + - name: Cache Go modules and build cache + uses: actions/cache@v4 with: - name: coverage-report - path: coverage.out + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: go-build-${{ runner.os }}-${{ hashFiles('go.sum') }} + restore-keys: | + go-build-${{ runner.os }}- + + - name: Download Go modules + run: go mod download + + - name: Build binary + run: go build -o naviwatcher ./cmd/naviwatcher + + docker: + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') + permissions: + packages: write + contents: read + steps: + - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Gitea Container Registry + - name: Login to Gitea Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} - username: ${{ secrets.REGISTRY_USERNAME }} - password: ${{ secrets.REGISTRY_PASSWORD }} + username: ${{ gitea.repository_owner }} + password: ${{ secrets.PACKAGES_TOKEN }} - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha - - - name: Build and push Docker image + - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-buildcache - cache-to: type=inline,mode=max \ No newline at end of file + tags: | + ${{ env.REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache,mode=max \ No newline at end of file diff --git a/docs/FILTER_FLOW.md b/docs/FILTER_FLOW.md new file mode 100644 index 0000000..e9cba15 --- /dev/null +++ b/docs/FILTER_FLOW.md @@ -0,0 +1,74 @@ +# Filter Flow Documentation + +This document explains how the `ignore_singles` and `ignore_compilations` settings propagate through the NaviWatcher system. + +## Overview + +The application implements a dual-path filtering approach to balance storage efficiency with real-time responsiveness to user preference changes: + +1. **MusicBrainz Sync Path (Store-time filtering)**: + - Applies filtering when caching release groups from the MusicBrainz API + - Stores only filtered results in the `external_releases` table + - Reduces database size by excluding unwanted release types upfront + +2. **Scanner Path (Read-time filtering)**: + - Applies filtering when retrieving cached data for comparison + - Ensures changes to `ignore_singles`/`ignore_compilations` take effect immediately + - Provides real-time responsiveness without waiting for cache expiry + +## Data Flow + +1. **Artist Settings Storage**: + - Navidrome artist sync populates the `artist_settings` table + - Stores `ignore_singles` and `ignore_compilations` boolean flags per artist + +2. **MusicBrainz Synchronization**: + - `SyncArtistDiscography` retrieves artist settings via `getArtistFilterOptions` + - Applies filtering using `ApplyTypeToggles` before storing in `external_releases` + - On cache hits, re-applies filter to ensure freshness of user preferences + +3. **Scanning Process**: + - `ScanArtist settings via `GetArtistSettings` + - Creates `TypeFilter` with current `ignore_singles`/`ignore_compilations` values + - Applies filter during `FindMissingReleases` via `filter.suppressed()` check + - Uses same `musicbrainz.ApplyTypeToggles` logic for consistency + +## Implementation Details + +### Shared Filtering Logic + +Both paths use the same underlying filtering logic: +- `musicbrainz.ApplyTypeToggles` for ExternalRelease filtering +- `musicbrainz.ApplyTypeTogglesToReleaseGroups` for ReleaseGroup filtering +- Consistent criteria: + - IgnoreSingles: Type == "Single" OR Type == "EP" OR SecondaryTypes contains "Single"/"EP" + - IgnoreCompilations: Type == "Compilation" OR SecondaryTypes contains "Compilation" + +### TypeFilter Structure + +```go +type TypeFilter struct { + IgnoreSingles bool + IgnoreCompilations bool +} +``` + +### Filter Application + +- **MusicBrainz Path**: `ApplyTypeToggles(releases, FilterOptions{IgnoreSingles: s.IgnoreSingles, IgnoreCompilations: s.IgnoreCompilations})` +- **Scanner Path**: `filter.suppressed(ext)` which internally uses the same logic + +## Benefits + +1. **Storage Efficiency**: Only desired release types are cached in the database +2. **Immediate Responsiveness**: User preference changes take effect in real-time +3. **Consistent Behavior**: Both code paths produce identical filtering results +4. **Cache Efficiency**: Existing cached data remains useful when preferences change + +## Related Files + +- `internal/scanner/diff.go`: TypeFilter definition and suppressed() method +- `internal/scanner/scan.go`: ScanArtist and ScanAll functions +- `internal/musicbrainz/sync.go`: SyncArtistDiscography and getArtistFilterOptions +- `internal/musicbrainz/filter.go`: ApplyTypeToggles and related filtering functions +- `internal/database/artist_settings.go`: GetArtistSettings and GetAllArtistSettings \ No newline at end of file