diff --git a/server/src/at_record_server/handlers/browse.gleam b/server/src/at_record_server/handlers/browse.gleam index f316120..188bc01 100644 --- a/server/src/at_record_server/handlers/browse.gleam +++ b/server/src/at_record_server/handlers/browse.gleam @@ -1,7 +1,6 @@ -//// HTTP glue for cross-user browsing: session handling, the fan-out read via -//// the `browse` domain module, and the add-from-browse write. Adding from -//// browse writes a genesis event that references the already-known canonical -//// release, skipping the adopt-or-mint discovery the manual add path runs. +//// HTTP glue for cross-user browsing: the `catalog_index` read (filtered by +//// `q`/`genre`) and the add-from-browse write, which references the known +//// canonical release and skips the manual path's adopt-or-mint discovery. import at_record/gen/catalog/release as catalog_release import at_record/gen/defs @@ -27,23 +26,18 @@ import gleam/option.{None, Some} import gleam/string import wisp.{type Request, type Response} -// --- GET /api/browse --- - pub fn browse(req: Request, ctx: Context) -> Response { use id, session <- require_session(req, ctx) use client, session <- with_pds_client(ctx, id, session) - // The crate load is best-effort: a miss just means no owned/wanted badges. let own = case event_log.load(client, session) { Ok(stored) -> browse_domain.ownership(stored) Error(_) -> browse_domain.empty_ownership() } - // Sort before dedup so "newest wins" is a property of our own code, not an - // assumption about how PDSes happen to order listRecords. + let query = wisp.get_query(req) let rows = - ctx.known_users.list() - |> list.flat_map(fn(user) { - browse_domain.fetch_user_releases(ctx.atproto.client, user) - }) + ctx.catalog_index.list() + |> filter_query(list.key_find(query, "q")) + |> filter_genre(list.key_find(query, "genre")) |> list.sort(fn(a, b) { string.compare(b.created_at, a.created_at) }) |> browse_domain.dedup |> list.take(100) @@ -52,6 +46,40 @@ pub fn browse(req: Request, ctx: Context) -> Response { |> wisp.json_response(200) } +fn filter_query( + rows: List(browse_domain.BrowseRow), + q: Result(String, Nil), +) -> List(browse_domain.BrowseRow) { + case q { + Ok(q) if q != "" -> list.filter(rows, matches_query(_, q)) + _ -> rows + } +} + +fn matches_query(row: browse_domain.BrowseRow, q: String) -> Bool { + let needle = string.lowercase(q) + string.contains(string.lowercase(row.title), needle) + || case row.artist_display { + Some(a) -> string.contains(string.lowercase(a), needle) + None -> False + } +} + +fn filter_genre( + rows: List(browse_domain.BrowseRow), + genre: Result(String, Nil), +) -> List(browse_domain.BrowseRow) { + case genre { + Ok(genre) if genre != "" -> list.filter(rows, matches_genre(_, genre)) + _ -> rows + } +} + +fn matches_genre(row: browse_domain.BrowseRow, genre: String) -> Bool { + let needle = string.lowercase(genre) + list.any(row.genres, fn(g) { string.contains(string.lowercase(g), needle) }) +} + pub fn encode_row( own: browse_domain.Ownership, row: browse_domain.BrowseRow, @@ -82,8 +110,6 @@ pub fn encode_row( ) } -// --- POST /api/browse/add --- - type AddForm { AddForm(uri: String, cid: String, status: String) } @@ -111,7 +137,6 @@ fn do_add( form: AddForm, ) -> Response { use client, session <- with_pds_client(ctx, id, session) - // Verify-before-trust: the index can be stale, so re-fetch and pin its cid. case ctx.catalog.fetch_release(form.uri) { None -> error_json(404, "release not found") Some(#(cid, value)) -> diff --git a/server/src/at_record_server/handlers/debug_index.gleam b/server/src/at_record_server/handlers/debug_index.gleam deleted file mode 100644 index 7382796..0000000 --- a/server/src/at_record_server/handlers/debug_index.gleam +++ /dev/null @@ -1,69 +0,0 @@ -//// Prototype-only comparison endpoint for `catalog_index`: a plain debug -//// route (not a lexicon method) for diffing the in-memory index against the -//// live `/browse` fan-out while validating the indexer spike. Shares -//// `handlers/browse`'s row shape and ownership computation exactly, so a -//// caller can point the browse grid at either endpoint interchangeably. - -import at_record_server/browse as browse_domain -import at_record_server/context.{type Context, require_session, with_pds_client} -import at_record_server/event_log -import at_record_server/handlers/browse.{encode_row} -import gleam/json -import gleam/list -import gleam/option.{None, Some} -import gleam/string -import wisp.{type Request, type Response} - -// --- GET /api/debug/index --- - -pub fn index(req: Request, ctx: Context) -> Response { - use id, session <- require_session(req, ctx) - use client, session <- with_pds_client(ctx, id, session) - // Best-effort, same as `/browse`: a miss just means no owned/wanted badges. - let own = case event_log.load(client, session) { - Ok(stored) -> browse_domain.ownership(stored) - Error(_) -> browse_domain.empty_ownership() - } - let query = wisp.get_query(req) - let rows = - ctx.catalog_index.list() - |> filter_query(list.key_find(query, "q")) - |> filter_genre(list.key_find(query, "genre")) - json.object([#("releases", json.array(rows, encode_row(own, _)))]) - |> json.to_string - |> wisp.json_response(200) -} - -fn filter_query( - rows: List(browse_domain.BrowseRow), - q: Result(String, Nil), -) -> List(browse_domain.BrowseRow) { - case q { - Ok(q) if q != "" -> list.filter(rows, matches_query(_, q)) - _ -> rows - } -} - -fn matches_query(row: browse_domain.BrowseRow, q: String) -> Bool { - let needle = string.lowercase(q) - string.contains(string.lowercase(row.title), needle) - || case row.artist_display { - Some(a) -> string.contains(string.lowercase(a), needle) - None -> False - } -} - -fn filter_genre( - rows: List(browse_domain.BrowseRow), - genre: Result(String, Nil), -) -> List(browse_domain.BrowseRow) { - case genre { - Ok(genre) if genre != "" -> list.filter(rows, matches_genre(_, genre)) - _ -> rows - } -} - -fn matches_genre(row: browse_domain.BrowseRow, genre: String) -> Bool { - let needle = string.lowercase(genre) - list.any(row.genres, fn(g) { string.contains(string.lowercase(g), needle) }) -} diff --git a/server/src/at_record_server/router.gleam b/server/src/at_record_server/router.gleam index 9c519e5..242194d 100644 --- a/server/src/at_record_server/router.gleam +++ b/server/src/at_record_server/router.gleam @@ -5,7 +5,6 @@ import at_record_server/context.{type Context, error_json} import at_record_server/handlers/amend import at_record_server/handlers/browse import at_record_server/handlers/cover_proxy -import at_record_server/handlers/debug_index import at_record_server/handlers/discogs import at_record_server/handlers/edit_inbox import at_record_server/handlers/oauth @@ -35,7 +34,6 @@ pub fn handle_request(req: Request, ctx: Context) -> Response { ["api", "oauth", "callback"], Get -> oauth.callback(req, ctx) ["api", "discogs", "connect"], Get -> discogs.connect(req, ctx) ["api", "discogs", "callback"], Get -> discogs.callback(req, ctx) - ["api", "debug", "index"], Get -> debug_index.index(req, ctx) ["api", "cover", did, cid], Get -> cover_proxy.serve(did, cid, ctx) ["xrpc", nsid], method -> dispatch_xrpc(req, ctx, nsid, method) ["api", ..], _ -> error_json(404, "not found") diff --git a/web/src/at_record_web/effects.gleam b/web/src/at_record_web/effects.gleam index 23f2c09..fc17af2 100644 --- a/web/src/at_record_web/effects.gleam +++ b/web/src/at_record_web/effects.gleam @@ -560,9 +560,9 @@ pub fn load_browse() -> Effect(Msg) { ) } -/// The debug substring/genre index search, shaped to match `/browse` exactly -/// (same decoder), so a search result drops straight into `model.browse`. -/// Not `xrpc()`: this is a plain `/api` route, not a lexicon method. +/// The substring/genre search over `catalog.listReleases`, shaped to match +/// `load_browse` exactly (same decoder), so a search result drops straight +/// into `model.browse`. pub fn search_browse(query: String, genre: String) -> Effect(Msg) { let q_param = case string.trim(query) { "" -> "" @@ -575,8 +575,8 @@ pub fn search_browse(query: String, genre: String) -> Effect(Msg) { let params = [q_param, genre_param] |> list.filter(fn(p) { p != "" }) |> string.join("&") let url = case params { - "" -> "/api/debug/index" - _ -> "/api/debug/index?" <> params + "" -> xrpc("catalog.listReleases") + _ -> xrpc("catalog.listReleases") <> "?" <> params } rsvp.get( url,