{"id":51317881,"url":"https://github.com/vllnt/convex-music","last_synced_at":"2026-07-01T09:31:13.462Z","repository":{"id":367588514,"uuid":"1271486684","full_name":"vllnt/convex-music","owner":"vllnt","description":"Provider-neutral, cached music catalog as a Convex component — Spotify, Apple Music, and more behind one API","archived":false,"fork":false,"pushed_at":"2026-06-26T15:42:04.000Z","size":540,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-26T17:17:53.351Z","etag":null,"topics":["apple-music","convex","convex-component","music","music-catalog","spotify","typescript","vllnt"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@vllnt/convex-music","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vllnt.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":"bntvllnt"}},"created_at":"2026-06-16T17:58:33.000Z","updated_at":"2026-06-26T15:48:42.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/vllnt/convex-music","commit_stats":null,"previous_names":["vllnt/convex-music"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/vllnt/convex-music","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-music","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-music/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-music/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-music/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vllnt","download_url":"https://codeload.github.com/vllnt/convex-music/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vllnt%2Fconvex-music/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35001648,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-01T02:00:05.325Z","response_time":130,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apple-music","convex","convex-component","music","music-catalog","spotify","typescript","vllnt"],"created_at":"2026-07-01T09:31:11.120Z","updated_at":"2026-07-01T09:31:13.455Z","avatar_url":"https://github.com/vllnt.png","language":"TypeScript","funding_links":["https://github.com/sponsors/bntvllnt"],"categories":[],"sub_categories":[],"readme":"\u003c!-- Badges --\u003e\n\n[![Convex Component](https://img.shields.io/badge/convex-component-EE342F.svg)](https://convex.dev/components)\n[![npm](https://img.shields.io/npm/v/@vllnt/convex-music.svg)](https://www.npmjs.com/package/@vllnt/convex-music)\n[![CI](https://github.com/vllnt/convex-music/actions/workflows/ci.yml/badge.svg)](https://github.com/vllnt/convex-music/actions/workflows/ci.yml)\n[![license](https://img.shields.io/npm/l/@vllnt/convex-music.svg)](LICENSE)\n\n# @vllnt/convex-music\n\nA provider-neutral, cached music catalog for Convex apps — look up tracks, artists, and albums\nfrom Spotify, Apple Music, and more through one typed API, with results cached in the component's\nown sandboxed tables.\n\n```ts\nconst music = new Music(components.music);\n\n// Cache a provider's normalized facts for an entity (TTL-bounded)…\nawait music.put(ctx, { kind: \"track\", provider: \"spotify\", externalId: id, isrc, value });\n// …then read it back fast on the next request.\nconst hit = await music.get(ctx, { kind: \"track\", provider: \"spotify\", externalId: id });\n```\n\n## Features\n\n- One typed API across providers — Spotify and Apple Music today; Deezer, MusicBrainz, and\n  Wikidata as drop-in adapters.\n- Sandboxed cache tables with a per-entry TTL — dedupe provider calls and ease rate limits.\n- Tracks, artists, and albums, cached by opaque provider id and cross-referenced by ISRC.\n- `getByIsrc` resolves the same recording across every provider you've cached.\n- `pruneExpired` is an idempotent sweep, safe to run on a schedule.\n- Stores only public catalog facts — no secrets or credentials.\n- `[planned]` Owns a durable music catalog (artists / tracks / playlists) populated from providers\n  and read via API; your app keeps gameplay + editorial, referencing catalog rows by id / ISRC.\n- `[planned]` In-component import / sync / repair engine, driven by mount policy.\n- `[planned]` Provider fetch/search adapters (`search`, `getTrack` / `getArtist` / `getAlbum`).\n- `[planned]` Field-source policy — configure what comes back from search + catalog reads: which\n  entity kinds, which fields, and — for **every field independently** — which provider(s) supply it:\n  one, an ordered pick, an explicit subset (e.g. 3 of 4 preview URLs), or all. Multi-select returns a\n  provider-keyed map, so adding providers never changes a field's type. Default at mount, override per call.\n- `[planned]` Multiple catalogs per mount — one `app.use` holds many catalogs (e.g. artists, tracks),\n  each with its own providers + field-source policy; a default catalog keeps single use zero-config.\n  Named mounts remain available for hard isolation.\n- `[planned]` Pluggable providers — add a provider as one adapter (its raw schema + a mapper to the\n  internal normalized schema) registered in the registry; no core changes.\n- `[planned]` Artist-image auto-sync with a configurable provider-selection policy.\n\n## Installation\n\n```bash\nnpm install @vllnt/convex-music\n```\n\nRequires `convex@^1.41.0` as a peer dependency.\n\n### Environment variables `[planned]`\n\nProvider credentials are supplied as [Convex environment variables](https://docs.convex.dev/production/environment-variables) on your deployment (used once the provider adapters ship):\n\n| Provider | Variables |\n| --- | --- |\n| Spotify | `SPOTIFY_CLIENT_ID`, `SPOTIFY_CLIENT_SECRET` |\n| Apple Music | `APPLE_MUSIC_ISSUER`, `APPLE_MUSIC_KID`, `APPLE_MUSIC_PRIVATE_KEY` |\n\nWhich providers are enabled (and their preference order) is set in the mount policy, not via env flags.\n\n## Usage\n\nMount the component in your app's `convex.config.ts`:\n\n```ts\nimport { defineApp } from \"convex/server\";\nimport music from \"@vllnt/convex-music/convex.config\";\n\nconst app = defineApp();\napp.use(music);\nexport default app;\n```\n\nThen use the client from your own queries and mutations. The host owns auth — gate the write\nmethods behind your own authorized functions, and persist results into your own domain tables:\n\n```ts\nimport { Music } from \"@vllnt/convex-music\";\nimport { components } from \"./_generated/api\";\n\nconst music = new Music(components.music);\n\nexport const cacheTrack = internalMutation({\n  args: { externalId: v.string(), isrc: v.string(), value: trackValue },\n  handler: (ctx, args) =\u003e\n    music.put(ctx, {\n      kind: \"track\",\n      provider: \"spotify\",\n      externalId: args.externalId,\n      isrc: args.isrc,\n      value: args.value,\n      ttlMs: 1000 * 60 * 60 * 24, // refresh daily\n    }),\n});\n```\n\n## API Reference\n\n| Method | Kind | Description |\n| --- | --- | --- |\n| `put(ctx, input)` | mutation | Cache (insert or refresh) one provider's facts; returns the entry id. |\n| `get(ctx, key)` | query | Fetch one cached entry, or `null` if missing or expired. |\n| `getByIsrc(ctx, isrc)` | query | Every fresh cached track for an ISRC, across providers. |\n| `invalidate(ctx, key)` | mutation | Drop one cached entry; returns whether a row was deleted. |\n| `pruneExpired(ctx)` | mutation | Delete every expired entry; returns the count removed. |\n| `stats(ctx)` | query | Count of cached entries. |\n\nFull reference — signatures, value shapes, and error codes: [`docs/API.md`](docs/API.md).\n\n## React [planned]\n\n\u003e Shipped in the `./react` entry; wraps the **planned** catalog query surface (not in 0.1.0).\n\nOptional, tree-shakeable hooks over `convex/react`. The host re-exports its own catalog query\nrefs and passes them in — the component never owns the host's `api`. `react` + `convex/react`\nare optional peer deps, so a backend-only consumer pulls in zero React.\n\n```tsx\nimport { useArtist, useSearchTracks } from \"@vllnt/convex-music/react\";\nimport { api } from \"../convex/_generated/api\"; // your wrappers re-exporting the component queries\n\nconst artist = useArtist(api.music.getArtist, artistId);\nconst tracks = useSearchTracks(api.music.searchTracks, query, 20);\n```\n\n| Hook | Returns | Description |\n| --- | --- | --- |\n| `useArtist(ref, id)` | `CatalogArtist \\| null` | Reactively read one unified artist by id. |\n| `useTrack(ref, id)` | `CatalogTrack \\| null` | Reactively read one unified track by id. |\n| `useArtistImage(ref, provider, id, policy?)` | `string \\| null` | Project an artist image per a field-source policy. |\n| `useTrackPreview(ref, provider, id, policy?)` | `string \\| null` | Project a track preview URL per a field-source policy. |\n| `useSearchArtists(ref, query, limit?)` | `CatalogArtist[]` | Reactively search artists by name. |\n| `useSearchTracks(ref, query, limit?)` | `CatalogTrack[]` | Reactively search tracks by title. |\n\n## Security\n\n- The component caches only public catalog facts — no secrets or credentials are stored.\n- Provider credentials live in the host's environment; they never reach the client.\n- The host gates every write method behind its own authorized functions.\n\nSee [`SECURITY.md`](SECURITY.md) for vulnerability reporting.\n\n## Testing\n\n```bash\npnpm test            # vitest + convex-test + @edge-runtime/vm\npnpm test:coverage   # 100% coverage gate\n```\n\n## Contributing\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md). Issues and pull requests welcome.\n\n## Author\n\nBuilt by [bntvllnt](https://github.com/bntvllnt) · [bntvllnt.com](https://bntvllnt.com) · [X @bntvllnt](https://x.com/bntvllnt)\n\nPart of the [@vllnt](https://github.com/vllnt) Convex component fleet — [vllnt.com](https://vllnt.com)\n\nIf this is useful, [sponsor the work](https://github.com/sponsors/bntvllnt).\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvllnt%2Fconvex-music","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvllnt%2Fconvex-music","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvllnt%2Fconvex-music/lists"}