Skip to content

feat(explore): User collection & wantlist graph queries and UI #59

@SimplicityGuy

Description

@SimplicityGuy

Overview

Extend the explore service with authenticated, user-aware graph queries leveraging the COLLECTED and WANTS relationships created by the collector service (#58). Also adds frontend UI tabs for personal collection browsing, wantlist, and recommendations.

Reference Implementation

vinyldigger has the data model and search/match patterns:

  • backend/src/models/search_result.pyis_in_collection, is_in_wantlist boolean flags on results
  • backend/src/services/search.py — matching logic between search results and collection/wantlist

The Neo4j query layer is specific to discogsography's graph model.

New API Endpoints

All require Authorization: Bearer <jwt> header. Added to the existing explore service.

Collection Queries

GET /api/user/collection
    → Paginated list of user's collected releases
    → Params: page, per_page, sort (date_added|rating|title|artist|year), genre, decade, label
    → Returns: releases with full graph context (artists, labels, genres)

GET /api/user/collection/graph
    → D3.js nodes/links structure of collected releases + connected artists/labels
    → For collection visualization

GET /api/user/collection/stats
    → {
        total: int,
        by_genre: [{genre, count}],
        by_decade: [{decade, count}],
        by_label: [{label, count}],
        by_artist: [{artist, count}],
        avg_rating: float
      }

Wantlist Queries

GET /api/user/wantlist
    → Paginated list of user's wanted releases

GET /api/user/wantlist/stats
    → {total, by_genre, by_decade, by_artist, avg_rating}

Discovery & Recommendations

GET /api/user/recommendations
    → Releases to consider based on collection graph traversal
    → Ranked by: number of collected artists/labels that also released candidate

GET /api/user/collection/artists/gaps
    → For each artist in your collection: releases you don't own
    → "You have 3 Aphex Twin releases; here are 5 you don't"

GET /api/user/wantlist/context
    → Wantlist items enriched with graph context
    → Shows: other releases from same artist/label you already own

Cross-Check (from vinyldigger is_in_collection / is_in_wantlist pattern)

GET /api/releases/{discogs_release_id}/ownership
    → {in_collection: bool, in_wantlist: bool, collection_instance_count: int}
    → Useful for decorating existing explore results with personal flags

Neo4j Queries

Collection Stats by Genre

MATCH (u:User {id: $user_id})-[:COLLECTED]->(r:Release)-[:HAS_GENRE]->(g:Genre)
RETURN g.name AS genre, count(r) AS count
ORDER BY count DESC

Collection by Decade

MATCH (u:User {id: $user_id})-[:COLLECTED]->(r:Release)
WHERE r.released IS NOT NULL
RETURN (toInteger(r.released) / 10) * 10 AS decade, count(r) AS count
ORDER BY decade

Recommendations (artists you collect → their other releases)

MATCH (u:User {id: $user_id})-[:COLLECTED]->(owned:Release)<-[:RELEASED]-(a:Artist)
MATCH (a)-[:RELEASED]->(candidate:Release)
WHERE NOT (u)-[:COLLECTED]->(candidate)
  AND NOT (u)-[:WANTS]->(candidate)
WITH candidate, collect(DISTINCT a.name) AS via_artists, count(DISTINCT a) AS artist_overlap
ORDER BY artist_overlap DESC
LIMIT 20
RETURN candidate, via_artists, artist_overlap

Artist Gap Analysis

MATCH (u:User {id: $user_id})-[:COLLECTED]->(c:Release)<-[:RELEASED]-(a:Artist)
MATCH (a)-[:RELEASED]->(all_releases:Release)
WHERE NOT (u)-[:COLLECTED]->(all_releases)
WITH a, count(DISTINCT c) AS owned_count, collect(DISTINCT all_releases.title) AS missing
WHERE size(missing) > 0
RETURN a.name AS artist, owned_count, size(missing) AS missing_count, missing[..5] AS sample_missing
ORDER BY owned_count DESC

Wantlist Context (what you already own from same artist/label)

MATCH (u:User {id: $user_id})-[:WANTS]->(w:Release)
OPTIONAL MATCH (w)<-[:RELEASED]-(a:Artist)-[:RELEASED]->(also:Release)
  WHERE (u)-[:COLLECTED]->(also)
WITH w, collect(DISTINCT {artist: a.name, title: also.title}) AS related_owned
RETURN w, related_owned
ORDER BY size(related_owned) DESC

Ownership Check (vinyldigger is_in_collection / is_in_wantlist pattern)

MATCH (u:User {id: $user_id})
OPTIONAL MATCH (u)-[c:COLLECTED]->(r:Release {discogs_id: $release_id})
OPTIONAL MATCH (u)-[w:WANTS]->(r)
RETURN
    count(c) > 0 AS in_collection,
    count(c) AS instance_count,
    count(w) > 0 AS in_wantlist

Integration with Existing Explore Queries

Decorate existing explore results (entity details, search, expand) with ownership flags:

# For authenticated requests, enrich release results with:
{
    "release": {...},          # existing explore data
    "ownership": {
        "in_collection": true,
        "instance_count": 2,   # might own multiple pressings
        "in_wantlist": false,
    }
}

This mirrors vinyldigger's is_in_collection and is_in_wantlist boolean fields on SearchResult.

Frontend UI Extensions

Add to the explore service's frontend:

"My Collection" Tab

  • Summary stats panel (total items, unique artists, unique labels, avg rating)
  • Genre breakdown chart (D3.js donut or bar)
  • Decade distribution histogram
  • D3.js graph: collected releases connected via shared artists/labels (colored by decade/genre)
  • Sortable/filterable list view

"Wantlist" Tab

  • Paginated wantlist with release info
  • "You also own from this artist" callouts (from wantlist context query)
  • Wantlist stats

"Discover" Tab

  • Recommendation list ranked by artist overlap
  • "Complete your collection" artist gap analysis
  • Filters by genre, decade, label

Collection Ownership Decorations

  • Existing explore graph nodes for releases show collection/wantlist badges when authenticated
  • Detail panels include ownership status

Auth UI

  • Login/register from explore service navigation
  • User settings: Discogs connection status + "Sync now" button (triggers collector)

Performance Considerations

  • All user queries must use User.id index (UUID lookup, not scan)
  • COLLECTED and WANTS relationships indexed by user_id in Neo4j
  • Paginate all list endpoints (default per_page: 50, max: 100)
  • For ownership decoration on existing queries: batch check multiple release IDs in single Cypher query
  • Cache stats results in Redis (short TTL, e.g. 5 minutes)

Acceptance Criteria

  • All user endpoints return 401 for unauthenticated requests
  • All user endpoints return 403 if Discogs not connected or no sync run yet
  • /collection returns correct paginated data matching user_collections table
  • /collection/stats returns accurate breakdowns by genre, decade, label, artist
  • /wantlist returns correct data
  • /recommendations returns non-owned, non-wanted releases from known artists, ranked
  • /collection/artists/gaps returns correct gap analysis
  • /wantlist/context shows related owned items from same artists
  • Ownership decoration works on existing release detail endpoints
  • D3.js collection graph renders and is navigable
  • All Cypher queries use indexed lookups (no full graph scans)
  • Stats results cached in Redis with appropriate TTL
  • Pagination consistent with existing explore endpoints

Dependencies

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpythonPull requests that update Python code

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions