Commit Graph

88 Commits

Author SHA1 Message Date
Claudio Ortolina 0dcd6eab83 Replace /scrobble/:release_id with nested route 2026-04-23 14:39:26 +01:00
Claudio Ortolina d8041dc0dd Add /scrobble/:rg_id release-group page 2026-04-23 14:35:58 +01:00
Claudio Ortolina 4194fb8b6d Update dependencies
oban_web 2.12.2 => 2.12.3
2026-04-15 13:50:52 +01:00
Claudio Ortolina 1cf9446063 Update oban_web to 2.12
- Had to disable nonces for style-src directives (seems like they're not
  propagated everywhere in oban_web's templates)
- Doesn't seem to support crons or workflows when backed by SQLite
2026-03-26 10:09:22 +00:00
Claudio Ortolina 1ee4d3465a Move /dev/maintenance to /maintenance 2026-03-18 07:44:22 +00:00
Claudio Ortolina 69ff926aba Allow WASM evaluation on prod 2026-03-14 18:02:27 +00:00
Claudio Ortolina 2dd6f10a7f Fix CSP error for barcode detection 2026-03-14 17:38:04 +00:00
Claudio Ortolina 854a6e1816 Allow worker blob URLs in CSP directives
canvas-confetti creates web workers from blob URLs,
which requires an explicit worker-src directive.
2026-03-13 15:01:28 +00:00
Claudio Ortolina 145c5c288c Allow loading img blobs in csp directives 2026-03-13 14:47:35 +00:00
Claudio Ortolina 3ddb287187 Fix csp directives for online store template logos 2026-03-09 18:30:29 +00:00
Claudio Ortolina f9845e8b7c Dry up CSP policies 2026-03-08 19:56:05 +00:00
Claudio Ortolina 77c45bddc6 Update CSP policies for musicbrainz covers + fallbacks 2026-03-08 19:54:00 +00:00
Claudio Ortolina 27519fca29 Fix CSP for images from brave 2026-03-08 19:47:43 +00:00
Claudio Ortolina 59616e58b5 Allow LiveDebugger origins in dev 2026-03-08 19:44:41 +00:00
Claudio Ortolina 1bfe501b3d Split strings for readability 2026-03-07 23:44:00 +00:00
Claudio Ortolina 05354b9f78 Explain the CSP policies 2026-03-07 23:40:40 +00:00
Claudio Ortolina 46da8eea38 EXP: fix CSP issues for /dev routes 2026-03-07 21:07:24 +00:00
Claudio Ortolina 41480316c2 Fix CSP settings
⏺ Here's a breakdown of each directive:

  ┌─────────────────┬────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  │    Directive    │                     Value                      │                                                         Purpose                                                         │
  ├─────────────────┼────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ default-src     │ 'self'                                         │ Fallback for any resource type not explicitly listed — only allow from the app's own origin                             │
  ├─────────────────┼────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ script-src      │ 'self'                                         │ JavaScript can only be loaded from the app's origin (no external scripts, no inline)                                    │
  ├─────────────────┼────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ style-src       │ 'self' 'unsafe-inline' https://rsms.me         │ Stylesheets from the app's origin, inline styles (needed by LiveView), and the https://rsms.me/inter/ hosted on rsms.me │
  ├─────────────────┼────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ font-src        │ 'self' https://rsms.me                         │ Font files from the app's origin and rsms.me (Inter font)                                                               │
  ├─────────────────┼────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ img-src         │ 'self' data: https://lastfm.freetls.fastly.net │ Images from the app's origin, data: URIs (inline base64 images), and Last.fm's CDN for album/artist artwork             │
  ├─────────────────┼────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ connect-src     │ 'self'                                         │ XHR/fetch/WebSocket connections only to the app's origin (covers LiveView's WebSocket)                                  │
  ├─────────────────┼────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ frame-ancestors │ 'self'                                         │ The app can only be embedded in iframes by itself (clickjacking protection)                                             │
  ├─────────────────┼────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ base-uri        │ 'self'                                         │ Restricts <base> tag to the app's origin (prevents base tag injection attacks)                                          │
  └─────────────────┴────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

  Notable things not allowed by this policy:
  - No inline <script> tags or eval()
  - No external JavaScript from CDNs
  - No embedding in third-party iframes
  - No connections to external APIs from the browser (all external API calls go through the server)
2026-03-07 19:13:48 +00:00
Claudio Ortolina 3812b6c402 Add Content-Security-Policy header
Closes #100
2026-03-07 18:53:52 +00:00
Claudio Ortolina 23c478fe4c Mount telemetry repo in live dashboard stats 2026-03-05 13:52:55 +00:00
Claudio Ortolina 73a943bc63 Server records on this day email assets from a public endpoint 2026-03-05 10:50:35 +00:00
Claudio Ortolina 478832249b Send record of the day email 2026-03-05 10:37:04 +00:00
Claudio Ortolina 1b934537ea Remove Sentry and Honeybadger 2026-02-26 21:52:57 +00:00
Claudio Ortolina 18ac442b2c Install ErrorTracker 2026-02-26 21:31:37 +00:00
Claudio Ortolina 77c5453fa8 Report all errors to Honeybadger 2026-02-26 20:47:16 +00:00
Claudio Ortolina 68956978e1 Add show route for record sets 2026-02-07 16:09:26 +00:00
Claudio Ortolina 419b093101 First pass at record sets 2026-02-05 11:46:52 +00:00
Claudio Ortolina 6f4fe163df Handle toast messages from inside Release component 2026-01-27 16:16:28 +00:00
Claudio Ortolina 11acec56b2 Remove lotus
Unstable - and it messes with the repo connection, creating other
errors.
2025-11-03 17:29:37 +00:00
Claudio Ortolina 299dee4b85 Add maintenance page
Some actions are missing
2025-11-02 21:02:34 +00:00
Claudio Ortolina f02564fa30 Add /api/collection/on_this_day endpoint 2025-10-24 20:06:23 +01:00
Claudio Ortolina a30e1b32d7 Mount data analysis via Lotus packages 2025-10-22 16:33:07 +01:00
Claudio Ortolina 57dfd3d33d Remove ErrorTracker
Doesn't work with recent LiveView releases, and seems to be maintained
very slowly despite incoming PRs to fix the issues.
2025-10-03 10:15:23 +03:00
Claudio Ortolina 67341f3ceb Revert "Add Orion for production perf measurement"
This reverts commit ec40f357d5.
2025-09-30 20:11:19 +03:00
Claudio Ortolina ec40f357d5 Add Orion for production perf measurement 2025-09-30 09:44:05 +03:00
Claudio Ortolina e9e4752bce EXP - scrobble anything 2025-09-24 08:55:42 +03:00
Claudio Ortolina 455c3a8b53 Serve artist images via assets 2025-09-18 21:26:20 +03:00
Claudio Ortolina e158345674 Change /covers to /assets 2025-09-18 21:26:16 +03:00
Claudio Ortolina b0f4e18ef7 Fix API collection record cover URLs 2025-09-17 11:24:14 +03:00
Claudio Ortolina 0996b78c7b Use Transforms to serve covers at the right size 2025-09-17 10:26:38 +03:00
Claudio Ortolina dae334b1b7 Scrobbled tracks CRUD
- Failing tests
- Warnings
2025-09-16 12:45:36 +03:00
Claudio Ortolina 21783f5dbf Resolve timezone from user browser 2025-09-11 22:14:15 +03:00
Claudio Ortolina bd12454884 Serve covers from assets 2025-09-01 14:44:17 +03:00
Claudio Ortolina 65de5fc833 Add first version of online stores for wishlisted records (claude)
See docs/plans/online_stores.md
2025-07-15 09:42:59 +01:00
Claudio Ortolina f57485e509 Allow manual upload of artist image 2025-07-06 16:57:30 +01:00
Claudio Ortolina d91685b646 First rough implementation of scrobble rules (Claude)
This plan outlines the implementation of functionality to manage
arbitrary rules for fixing data in the `scrobbled_tracks` table. The
system will support two types of transformations:

1. **Album Rule**: Update the album MusicBrainz ID of all tracks
belonging to a specific album (identified by album title)
2. **Artist Rule**: Update the artist MusicBrainz ID of all tracks
belonging to a specific artist (identified by artist name)

- `scrobbled_tracks` table exists with the following structure:
  - `scrobbled_at_uts` (integer, primary key)
  - `musicbrainz_id` (string) - track MusicBrainz ID
  - `title` (string) - track title
  - `cover_url` (string)
  - `scrobbled_at_label` (string)
  - `artist` (map) - embedded artist data with `musicbrainz_id` and
`name`
  - `album` (map) - embedded album data with `musicbrainz_id` and
`title`
  - `last_fm_data` (map) - raw Last.fm API response

- `LastFm.Track` - Schema for scrobbled tracks
- `LastFm.Artist` - Embedded artist schema
- `LastFm.Album` - Embedded album schema

- Main navigation has Stats, Collection, Wishlist sections
- "More" dropdown contains development tools and utilities
- Uses Phoenix LiveView for interactive components

**File**:
`priv/repo/migrations/YYYYMMDDHHMMSS_create_scrobble_rules.exs`

Create `scrobble_rules` table with:

- `id` (primary key)
- `type` (string) - "album" or "artist"
- `match_value` (string) - album title or artist name to match
- `target_musicbrainz_id` (string) - MusicBrainz ID to set
- `enabled` (boolean) - whether rule is active
- `description` (text) - optional description for the rule
- `inserted_at` (timestamp)
- `updated_at` (timestamp)

Indexes:

- `type, match_value` (compound index for efficient matching)
- `enabled` (for filtering active rules)

**File**: `lib/music_library/scrobble_rules/scrobble_rule.ex`

Create Ecto schema with:

- Field definitions matching migration
- Validations:
  - `type` must be "album" or "artist"
  - `match_value` required and non-empty
  - `target_musicbrainz_id` required and non-empty
  - `enabled` defaults to true
- Changeset functions for create/update operations

**File**: `lib/music_library/scrobble_rules.ex`

Implement context functions:

- `list_scrobble_rules(opts \\ [])` - list all rules with optional
filtering
- `get_scrobble_rule!(id)` - get specific rule by ID
- `create_scrobble_rule(attrs)` - create new rule
- `update_scrobble_rule(rule, attrs)` - update existing rule
- `delete_scrobble_rule(rule)` - delete rule
- `change_scrobble_rule(rule, attrs \\ %{})` - create changeset
- `list_enabled_rules()` - get only enabled rules for worker
- `apply_album_rule(rule)` - apply album transformation
- `apply_artist_rule(rule)` - apply artist transformation
- `apply_all_rules()` - apply all enabled rules

**File**: `lib/music_library_web/components/layouts/app.html.heex`

Add new navigation link in the "More" dropdown:

- Icon: `hero-adjustments-horizontal`
- Label: "Scrobble Rules"
- Route: `/scrobble-rules`
- Position: After "Oban" link, before separator

**File**: `lib/music_library_web/router.ex`

Add new LiveView routes in the authenticated scope:

- `/scrobble-rules` - index page
- `/scrobble-rules/new` - create new rule
- `/scrobble-rules/:id/edit` - edit existing rule

**File**: `lib/music_library_web/live/scrobble_rules_live/index.ex`

Implement LiveView with:

- List view showing all rules with status indicators
- Create/Edit forms with validation
- Delete confirmation
- Filter controls (by type, enabled/disabled)
- Manual rule application triggers
- Real-time updates via PubSub

**Template**:
`lib/music_library_web/live/scrobble_rules_live/index.html.heex`

- Table layout with rule details
- Action buttons (Edit, Delete, Apply)
- Form components for create/edit
- Status indicators for enabled/disabled rules

**File**:
`lib/music_library_web/live/scrobble_rules_live/form_component.ex`

Create reusable form component:

- Type selection (album/artist)
- Match value input with validation
- Target MusicBrainz ID input
- Description textarea
- Enable/disable toggle
- Save/Cancel actions

Where possible, use the components available at
<https://docs.fluxonui.com/1.1.4/overview.html>.

**File**: `lib/music_library/scrobble_rules/worker.ex`

Implement Oban worker:

- Scheduled to run periodically (e.g., every 30 minutes)
- Fetches all enabled rules
- Applies each rule in sequence
- Logs application results
- Handles errors gracefully
- Tracks last application time

**File**: `test/music_library/scrobble_rules_test.exs`

- Test all context functions
- Test rule application logic
- Test edge cases and error handling

**File**: `test/music_library/scrobble_rules/scrobble_rule_test.exs`

- Test schema validations
- Test changeset functions
- Test constraint validations

**File**: `test/music_library_web/live/scrobble_rules_live_test.exs`

- Test LiveView interactions
- Test form submissions
- Test real-time updates
- Test navigation and routing

**File**: `test/music_library/scrobble_rules/worker_test.exs`

- Test job execution
- Test rule application
- Test error handling
- Test scheduling

- Ensure atomic operations when applying rules
- Validate MusicBrainz IDs before applying
- Log all transformations for audit trail

- Index optimization for rule matching
- Batch processing for large datasets
- Rate limiting for external API calls

- Clear feedback on rule application
- Preview mode to show what would change
- Bulk operations for managing multiple rules
- Export/import functionality for rule sets

- Input validation and sanitization
- Rate limiting on rule creation
- Audit logging for all changes

```sql
-- scrobble_rules table
CREATE TABLE scrobble_rules (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  type TEXT NOT NULL CHECK (type IN ('album', 'artist')),
  match_value TEXT NOT NULL,
  target_musicbrainz_id TEXT NOT NULL,
  enabled BOOLEAN NOT NULL DEFAULT TRUE,
  description TEXT,
  inserted_at TIMESTAMP NOT NULL,
  updated_at TIMESTAMP NOT NULL
);

CREATE INDEX idx_scrobble_rules_type_match ON scrobble_rules(type,
match_value);
CREATE INDEX idx_scrobble_rules_enabled ON scrobble_rules(enabled);
```

```
lib/music_library/
├── scrobble_rules/
│   ├── scrobble_rule.ex          # Schema
│   └── worker.ex                 # Oban worker
└── scrobble_rules.ex             # Context

lib/music_library_web/
└── live/
    └── scrobble_rules_live/
        ├── index.ex              # LiveView
        ├── index.html.heex       # Template
        └── form_component.ex     # Form component

priv/repo/migrations/
└── YYYYMMDDHHMMSS_create_scrobble_rules.exs

test/
├── music_library/
│   ├── scrobble_rules_test.exs
│   └── scrobble_rules/
│       ├── scrobble_rule_test.exs
│       └── worker_test.exs
└── music_library_web/
    └── live/
        └── scrobble_rules_live_test.exs
```

1. Database migration and schema
2. Context module with basic CRUD operations
3. LiveView interface for rule management
4. Navigation and routing updates
5. Periodic worker implementation
6. Comprehensive testing
7. Documentation and deployment

This plan ensures a robust, maintainable solution that integrates
seamlessly with the existing Music Library application architecture.
2025-07-03 16:47:08 +01:00
Claudio Ortolina e678d533f5 Mount BackgroundRepo in Ecto stats 2025-06-01 16:34:58 +01:00
Claudio Ortolina 676c2e4290 Enable quokka with :module_directives rule 2025-05-28 19:41:57 +01:00
Claudio Ortolina bb4fce61c7 Extract StaticAssets hook with a single default live_session 2025-05-13 21:03:59 +01:00
Claudio Ortolina 7c547f2d80 Redirect to Last.fm and receive a token (unused) 2025-05-05 19:19:12 +01:00