{"id":17784944,"url":"https://github.com/mellkam/soundify","last_synced_at":"2025-06-21T15:05:54.729Z","repository":{"id":65588737,"uuid":"594763503","full_name":"MellKam/soundify","owner":"MellKam","description":"🎧 Lightweight integration with the Spotify Web API for modern Javascript runtimes ","archived":false,"fork":false,"pushed_at":"2024-07-15T11:47:57.000Z","size":869,"stargazers_count":28,"open_issues_count":3,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-21T16:55:57.268Z","etag":null,"topics":["api","deno","music","sdk","soundify","spotify","typescript","web"],"latest_commit_sha":null,"homepage":"https://npmjs.com/@soundify/web-api","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/MellKam.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-01-29T15:17:51.000Z","updated_at":"2025-05-20T23:31:50.000Z","dependencies_parsed_at":"2023-10-15T03:13:34.896Z","dependency_job_id":"e33cb317-910b-4a7b-958d-9b90711deb36","html_url":"https://github.com/MellKam/soundify","commit_stats":{"total_commits":157,"total_committers":3,"mean_commits":"52.333333333333336","dds":"0.12101910828025475","last_synced_commit":"052741c8382c7dbbd2cc82f596081b886ea54999"},"previous_names":[],"tags_count":62,"template":false,"template_full_name":null,"purl":"pkg:github/MellKam/soundify","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MellKam%2Fsoundify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MellKam%2Fsoundify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MellKam%2Fsoundify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MellKam%2Fsoundify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MellKam","download_url":"https://codeload.github.com/MellKam/soundify/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MellKam%2Fsoundify/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261143144,"owners_count":23115672,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["api","deno","music","sdk","soundify","spotify","typescript","web"],"created_at":"2024-10-27T08:22:31.002Z","updated_at":"2025-06-21T15:05:49.712Z","avatar_url":"https://github.com/MellKam.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cp align=\"center\"\u003e\n     \u003cimg align=\"center\" width=\"480px\" src=\"https://svgshare.com/i/rf9.svg\"\u003e\n  \u003c/p\u003e\n  \u003cp align=\"center\"\u003e\n    \u003ca href=\"https://www.npmjs.com/package/@soundify/web-api\"\u003e\n      \u003cimg alt=\"npm\" src=\"https://img.shields.io/npm/v/@soundify/web-api\"\u003e\n    \u003c/a\u003e\n\t\t\u003ca href=\"https://bundlejs.com/?q=%40soundify%2Fweb-api\u0026treeshake=%5B*%5D\"\u003e\n\t\t\t\u003cimg src=\"https://deno.bundlejs.com/?q=@soundify/web-api\u0026badge=minified\" alt=\"Size of package (minified)\" /\u003e\n\t\t\u003c/a\u003e\n    \u003ca href=\"https://github.com/mellkam/soundify\"\u003e\n      \u003cimg src=\"https://img.shields.io/github/stars/mellkam/soundify\" alt=\"Github stars\" /\u003e\n    \u003c/a\u003e\n\t\t\u003ca href=\"https://jsr.io/@soundify/web-api\"\u003e\n\t\t\t\u003cimg src=\"https://jsr.io/badges/@soundify/web-api/score\" alt=\"jsr registry score\" /\u003e\n\t\t\u003c/a\u003e\n  \u003c/p\u003e\n\u003c/div\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003cp align=\"center\"\u003e\n    Soundify is a lightweight and flexible library for interacting with the Spotify API, designed to work seamlessly with TypeScript and support all available runtimes.\n  \u003c/p\u003e\n\u003c/div\u003e\n\n\u003cp align=\"center\"\u003e\n \u003ca href=\"#getting-started\"\u003eGetting Started\u003c/a\u003e | \u003ca href=\"#error-handling-📛\"\u003eError handling\u003c/a\u003e | \u003ca href=\"#token-refreshing\"\u003eToken refreshing\u003c/a\u003e | \u003ca href=\"#pagination\"\u003ePagination\u003c/a\u003e\n\u003c/p\u003e\n\n## Installation\n\nThe package doesn't depend on runtime specific apis, so you should be able to\nuse it without any problems everywhere.\n\n```bash\nnpm install @soundify/web-api\n```\n\n```jsonc\n// deno.json\n{\n\t\"imports\": {\n\t\t\"@soundify/web-api\": \"https://deno.land/x/soundify/mod.ts\"\n\t}\n}\n```\n\nInstall from [JSR registry](https://jsr.io/@soundify/web-api)\n\n```bash\ndeno add @soundify/web-api\n```\n\n## Getting Started\n\nSoundify has a very simple structure. It consists of a `SpotifyClient` capable\nof making requests to the Spotify API, along with a set of functions (like\n`getCurrentUser`) that utilize the client to make requests to specific\nendpoints.\n\n```ts\nimport { getCurrentUser, search, SpotifyClient } from \"@soundify/web-api\";\n\nconst client = new SpotifyClient(\"YOUR_ACCESS_TOKEN\");\n\nconst me = await getCurrentUser(client);\nconsole.log(me);\n\nconst result = await search(client, \"track\", \"Never Gonna Give You Up\");\nconsole.log(result.tracks.items.at(0));\n```\n\nCompared to the usual OOP way of creating API clients, this approach has several\nadvantages. The main one is that it is _tree-shakable_. You only ship code you\nuse. This may be not that important for server-side apps, but I'm sure frontend\nusers will thank you for not including an extra 10kb of crappy js into your\nbundle.\n\n```ts\nimport {\n\tgetAlbumTracks,\n\tgetArtist,\n\tgetArtistAlbums,\n\tgetRecommendations,\n\tSpotifyClient,\n} from \"@soundify/web-api\";\n\nconst client = new SpotifyClient(\"YOUR_ACCESS_TOKEN\");\n\nconst radiohead = await getArtist(client, \"4Z8W4fKeB5YxbusRsdQVPb\");\nconsole.log(`Radiohead popularity - ${radiohead.popularity}`);\n\nconst pagingResult = await getArtistAlbums(client, radiohead.id, { limit: 1 });\nconst album = pagingResult.items.at(0)!;\nconsole.log(`Album - ${album.name}`);\n\nconst tracks = await getAlbumTracks(client, album.id, { limit: 5 });\nconsole.table(\n\ttracks.items.map((track) =\u003e ({\n\t\tname: track.name,\n\t\tduration: track.duration_ms,\n\t})),\n);\n\nconst recomendations = await getRecommendations(client, {\n\tseed_artists: [radiohead.id],\n\tseed_tracks: tracks.items.map((track) =\u003e track.id).slice(0, 4),\n\tmarket: \"US\",\n\tlimit: 5,\n});\nconsole.table(\n\trecomendations.tracks.map((track) =\u003e ({\n\t\tartist: track.artists.at(0)!.name,\n\t\tname: track.name,\n\t})),\n);\n```\n\n## Error handling 📛\n\n```ts\nimport { getCurrentUser, SpotifyClient, SpotifyError } from \"@soundify/web-api\";\n\nconst client = new SpotifyClient(\"INVALID_ACCESS_TOKEN\");\n\ntry {\n\tconst me = await getCurrentUser(client);\n\tconsole.log(me);\n} catch (error) {\n\tif (error instanceof SpotifyError) {\n\t\terror.status; // 401\n\n\t\tconst message = typeof error.body === \"string\"\n\t\t\t? error.body\n\t\t\t: error.body?.error.message;\n\t\tconsole.error(message); // \"Invalid access token\"\n\n\t\terror.response.headers.get(\"Date\"); // You can access the response here\n\n\t\tconsole.error(error);\n\t\t// SpotifyError: 401 Unauthorized (https://api.spotify.com/v1/me) : Invalid access token\n\t\treturn;\n\t}\n\n\t// If it's not a SpotifyError, then it's some type of network error that fetch throws\n\t// Or can be DOMException if you abort the request\n\tconsole.error(\"We're totally f#%ked!\");\n}\n```\n\n### Rate Limiting 🕒\n\nIf you're really annoying customer, Spotify may block you for some time. To know\nwhat time you need to wait, you can use `Retry-After` header, which will tell\nyou time in seconds.\n[More about rate limiting↗](https://developer.spotify.com/documentation/web-api/concepts/rate-limits)\n\nTo handle this automatically, you can use `waitForRateLimit` option in\n`SpotifyClient`. (it's disabled by default, because it may block your code for\nunknown time)\n\n```ts\nconst client = new SpotifyClient(\"YOUR_ACCESS_TOKEN\", {\n\twaitForRateLimit: true,\n\t// wait only if it's less than a minute\n\twaitForRateLimit: (retryAfter) =\u003e retryAfter \u003c 60,\n});\n```\n\n## Authorization\n\nSoundify doesn't provide any tools for authorization, because that would require\nto write whole oauth library in here. We have many other battle-tested oauth\nsolutions, like [oauth4webapi](https://github.com/panva/oauth4webapi) or\n[oidc-client-ts](https://github.com/authts/oidc-client-ts). I just don't see a\npoint in reinventing the wheel 🫤.\n\nDespite this, we have a huge directory of examples, including those for\nauthorization.\n[OAuth2 Examples↗](https://github.com/MellKam/soundify/tree/main/examples/oauth)\n\n### Token Refreshing\n\n```ts\nimport { getCurrentUser, SpotifyClient } from \"@soundify/web-api\";\n\n// if you don't have access token yet, you can pass null to first argument\nconst client = new SpotifyClient(null, {\n\t// but you have to provide a function that will return a new access token\n\trefresher: () =\u003e {\n\t\treturn Promise.resolve(\"YOUR_NEW_ACCESS_TOKEN\");\n\t},\n});\n\nconst me = await getCurrentUser(client);\n// client will call your refresher to get the token\n// and only then make the request\nconsole.log(me);\n\n// let's wait some time to expire the token ...\n\nconst me = await getCurrentUser(client);\n// client will receive 401 and call your refresher to get new token\n// you don't have to worry about it as long as your refresher is working\nconsole.log(me);\n```\n\n## Pagination\n\nTo simplify the process of paginating through the results, we provide a\n`PageIterator` and `CursorPageIterator` classes.\n\n```ts\nimport { getPlaylistTracks, SpotifyClient } from \"@soundify/web-api\";\nimport { PageIterator } from \"@soundify/web-api/pagination\";\n\nconst client = new SpotifyClient(\"YOUR_ACCESS_TOKEN\");\n\nconst playlistIter = new PageIterator(\n\t(offset) =\u003e\n\t\tgetPlaylistTracks(client, \"37i9dQZEVXbMDoHDwVN2tF\", {\n\t\t\t// you can find the max limit for specific endpoint\n\t\t\t// in spotify docs or in the jsdoc comments of this property\n\t\t\tlimit: 50,\n\t\t\toffset,\n\t\t}),\n);\n\n// iterate over all tracks in the playlist\nfor await (const track of playlistIter) {\n\tconsole.log(track);\n}\n\n// or collect all tracks into an array\nconst allTracks = await playlistIter.collect();\nconsole.log(allTracks.length);\n\n// Want to get the last 100 items? No problem\nconst lastHundredTracks = new PageIterator(\n\t(offset) =\u003e\n\t\tgetPlaylistTracks(\n\t\t\tclient,\n\t\t\t\"37i9dQZEVXbMDoHDwVN2tF\",\n\t\t\t{ limit: 50, offset },\n\t\t),\n\t{ initialOffset: -100 }, // this will work just as `Array.slice(-100)`\n).collect();\n```\n\n```ts\nimport { getFollowedArtists, SpotifyClient } from \"@soundify/web-api\";\nimport { CursorPageIterator } from \"@soundify/web-api/pagination\";\n\nconst client = new SpotifyClient(\"YOUR_ACCESS_TOKEN\");\n\n// loop over all followed artists\nfor await (\n\tconst artist of new CursorPageIterator(\n\t\t(opts) =\u003e getFollowedArtists(client, { limit: 50, after: opts.after }),\n\t)\n) {\n\tconsole.log(artist.name);\n}\n\n// or collect all followed artists into an array\nconst artists = await new CursorPageIterator(\n\t(opts) =\u003e getFollowedArtists(client, { limit: 50, after: opts.after }),\n).collect();\n\n// get all followed artists starting from Radiohead\nconst artists = await new CursorPageIterator(\n\t(opts) =\u003e getFollowedArtists(client, { limit: 50, after: opts.after }),\n\t{ initialAfter: \"4Z8W4fKeB5YxbusRsdQVPb\" }, // let's start from Radiohead\n).collect();\n```\n\n## Other customizations\n\n```ts\nimport { SpotifyClient } from \"@soundify/web-api\";\n\nconst client = new SpotifyClient(\"YOUR_ACCESS_TOKEN\", {\n\t// You can use any fetch implementation you want\n\t// For example, you can use `node-fetch` in node.js\n\tfetch: (input, init) =\u003e {\n\t\treturn fetch(input, init);\n\t},\n\t// You can change the base url of the client\n\t// by default it's \"https://api.spotify.com/\"\n\tbeseUrl: \"https://example.com/\",\n\tmiddlewares: [(next) =\u003e (url, opts) =\u003e {\n\t\t// You can add your own middleware\n\t\t// For example, you can add some headers to every request\n\t\treturn next(url, opts);\n\t}],\n});\n```\n\n## Contributors ✨\n\nAll contributions are very welcome ❤️\n([emoji key](https://allcontributors.org/docs/en/emoji-key))\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --\u003e\n\u003c!-- prettier-ignore-start --\u003e\n\u003c!-- markdownlint-disable --\u003e\n\u003ctable\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"http://t.me/mellkam\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/51422045?v=4?s=100\" width=\"100px;\" alt=\"Artem Melnyk\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eArtem Melnyk\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"#maintenance-MellKam\" title=\"Maintenance\"\u003e🚧\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://github.com/danilluk1\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/51733612?v=4?s=100\" width=\"100px;\" alt=\"danluki\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003edanluki\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/MellKam/soundify/commits?author=danilluk1\" title=\"Code\"\u003e💻\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://lwjerri.dev\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/50290430?v=4?s=100\" width=\"100px;\" alt=\"Andrii Zontov\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eAndrii Zontov\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/MellKam/soundify/issues?q=author%3ALWJerri\" title=\"Bug reports\"\u003e🐛\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://braydenbabbitt.com\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/1495342?v=4?s=100\" width=\"100px;\" alt=\"Brayden Babbitt\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eBrayden Babbitt\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/MellKam/soundify/issues?q=author%3Abraydenbabbitt\" title=\"Bug reports\"\u003e🐛\u003c/a\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003c!-- markdownlint-restore --\u003e\n\u003c!-- prettier-ignore-end --\u003e\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:END --\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmellkam%2Fsoundify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmellkam%2Fsoundify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmellkam%2Fsoundify/lists"}