diff --git a/.env.test b/.env.test index 324a37d..9cf67ef 100644 --- a/.env.test +++ b/.env.test @@ -1,3 +1,4 @@ SPOTIFY_CLIENT_ID= SPOTIFY_CLIENT_SECRET= +SPOTIFY_REDIRECT_URI= SPOTIFY_REFRESH_TOKEN= \ No newline at end of file diff --git a/.github/renovate.json b/.github/renovate.json deleted file mode 100644 index 5b42d94..0000000 --- a/.github/renovate.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base", - ":disableRateLimiting", - "group:all" - ], - "rebaseWhen": "conflicted", - "gitAuthor": "Renovate Bot ", - "semanticCommitType": "chore", - "semanticCommitScope": "dependency", - "separateMajorMinor": false, - "lockFileMaintenance": { - "enabled": true - }, - "baseBranches": [ - "main" - ], - "ignorePaths": [ - "**/node_modules/**" - ] -} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 251f02e..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,81 +0,0 @@ -name: CI - -on: - push: - branches: - - main - - pull_request: - branches: - - main - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "18.x" - - - name: Setup pnpm - uses: pnpm/action-setup@v2 - with: - run_install: true - version: latest - - - name: Lint check - run: pnpm lint:check - - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "18.x" - - - name: Setup pnpm - uses: pnpm/action-setup@v2 - with: - run_install: true - version: latest - - - name: Test - run: pnpm test:coverage - env: - SPOTIFY_REFRESH_TOKEN: ${{ secrets.SPOTIFY_REFRESH_TOKEN }} - SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }} - SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }} - - - name: Collect coverage - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage/coverage.lcov - - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "18.x" - - - name: Setup pnpm - uses: pnpm/action-setup@v2 - with: - run_install: true - version: latest - - - name: Check types - run: pnpm types:check - - - name: Build - run: pnpm build diff --git a/.github/workflows/npm-publish.yaml b/.github/workflows/npm-publish.yaml deleted file mode 100644 index 620ed4d..0000000 --- a/.github/workflows/npm-publish.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Publish package to NPM -on: - push: - tags: - - "v**" - -jobs: - publish: - if: github.event.base_ref == 'refs/heads/main' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "18.x" - registry-url: "https://registry.npmjs.org" - scope: "@soundify" - - - name: Setup pnpm - uses: pnpm/action-setup@v2 - with: - run_install: true - version: latest - - - name: build and publish - run: pnpm build && pnpm publish --ignore-scripts --access public --no-git-checks - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..63f66a5 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,37 @@ +name: Release + +permissions: + contents: write + +on: + push: + tags: + - "v*" + +jobs: + release: + if: github.event.base_ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v3 + with: + node-version: "20.x" + registry-url: "https://registry.npmjs.org" + scope: "@soundify" + + - uses: pnpm/action-setup@v2 + with: + run_install: true + version: latest + + - run: pnpm build && pnpm publish --ignore-scripts --no-git-checks + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - run: npx changelogithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 186ab50..0000000 --- a/.prettierignore +++ /dev/null @@ -1,5 +0,0 @@ -.next -dist -types -coverage -node_modules \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index beb6303..0000000 --- a/.prettierrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "trailingComma": "none", - "tabWidth": 2, - "semi": true, - "singleQuote": false, - "printWidth": 80 -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index be030a2..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[markdown]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - } -} \ No newline at end of file diff --git a/README.md b/README.md index 5506c6c..ecf4371 100644 --- a/README.md +++ b/README.md @@ -6,426 +6,215 @@ npm - - license - - - Last commit - - - GitHub CI Status + + NPM downloads - - Code test coverage + + Github stars

- Soundify is a lightweight and flexible library for seamless communication with Spotify API, designed to work smoothly with TypeScript, Deno, Node.js, and client-side JavaScript. It's open source and provides an easy-to-use interface for accessing Spotify's data. + Soundify is a lightweight and flexible library for interacting with the Spotify API, designed to work seamlessly with TypeScript and support all available runtimes.

-# Features ✨ +

+ Getting Started | Authorization | Usage | Contributors +

-- 💻 Multiruntime: Works seamlessly with Node.js, Deno, and the Browser - environment. -- 🚀 Modern: Leverages native web APIs like `fetch`, `crypto`, - `URLSearchParams`, etc. -- 🔑 Comprehensive auth support: It can handle all Spotify Auth flows and - automatically refreshes access token. -- 📦 Lightweight and treeshakable: It is designed with care for your bundle - size. -- 🆎 Strictly typed: All entities returned by the api have exact and up to date - types. -- 📖 Great docs: The library comes with extensive documentation and lots of - examples. +## Installation -# Installation - -## [NPM](https://npmjs.com/package/@soundify/web-api) +The package doesn't depend on runtime specific apis, so you should be able to +use it without any problems everywhere. ```bash -npm i @soundify/web-api +pnpm add @soundify/web-api ``` -## Deno - -```ts -import { ... } from "npm:@soundify/web-api"; -``` - -# Gettings started - -To make your first request with Soundify you need to create a `SpotifyClient`. -As the first parameter it takes access token or -[AuthProvider](#auth-provider-and-automatic-tokens-refreshing). - -```ts -import { SpotifyClient } from "@soundify/web-api"; - -const client = new SpotifyClient("ACCESS_TOKEN"); -``` - -If you've used other api libraries, you can expect something like a bunch of -methods on a single class, but in our case the default recommendation is to use -endpoint functions that take the client as the first argument. In practice, it -looks like this: - -```ts -import { getCurrentUser, SpotifyClient } from "@soundify/web-api"; - -const client = new SpotifyClient("ACCESS_TOKEN"); -const user = await getCurrentUser(client); - -console.log(user); +```bash +bun install @soundify/web-api ``` -If your Access Token is valid it will output something like this - -```js +```jsonc +// deno.json { - "id": "31xofk5q7l22rvsbff7yiechyx6i", - "display_name": "Soundify", - "type": "user", - "uri": "spotify:user:31xofk5q7l22rvsbff7yiechyx6i", - // etc... + "imports": { + "@soundify/web-api": "https://deno.land/x/soundify/mod.ts" + } } ``` -This may be inconvenient for some users, but it was done primarily to allow tree -sharding so that clients don't send a lot of unused code. - -But, if you are writing a backend or don't care about the size of the library -you can use the `createSpotifyAPI()` function which will bind all the endpoint -functions to the client. That way you can use this object throughout your -application and not have to worry about imports. - -```ts -import { createSpotifyAPI, SpotifyClient } from "@soundify/web-api"; - -const api = createSpotifyAPI("ACCESS_TOKEN"); -const user = await api.getCurrentUser(); - -console.log(user); -``` - -# Authorization - -If you have no experience with Spotify Authorization you can read more about it -in the -[Spotify Authorization Guide](https://developer.spotify.com/documentation/web-api/concepts/authorization). - -There are four authorization flows that can be used in Spotify, and all of them -are supported in this library 🔥. The criteria for choosing the right flow for -you are described in the Spotify docs linked above. +## Getting Started -
-

Authorization Code flow

- -With this flow user grants permission only once, after which you can use refresh -token to create a new access token. The flow is used on the server because it -requires SPOTIFY_CLIENT_SECRET, which is not desirable to show to others. - -_Pseudo http-server code just for example_ +Soundify has a very simple structure. It consists of a `SpotifyClient` capable +of making requests to the Spotify API, along with a set of functions (like +`getCurrentUser`) that utilize the client to make requests to specific +endpoints. ```ts -import { AuthCode } from "@soundify/web-api"; +import { getCurrentUser, search, SpotifyClient } from "@soundify/web-api"; -const authFlow = new AuthCode({ - client_id: "YOUR_CLIENT_ID", - client_secret: "YOUR_CLIENT_SECRET" -}); - -const loginHandler = async (req, res) => { - const authURL = authFlow.getAuthURL({ - redirect_uri: "YOUR_REDIRECT_URI", - scopes: ["user-read-email"] - }); - res.redirect(302, authURL.toString()); -}; +const client = new SpotifyClient("YOUR_ACCESS_TOKEN"); -const codeHandler = async (req, res) => { - try { - const code = new URL(req.url).searchParams.get("code"); - if (!code) throw new Error("Unable to find 'code'"); - - const { access_token, refresh_token } = await authFlow.getGrantData( - "YOUR_REDIRECT_URI", - code - ); - res.cookie("refresh_token", refresh_token); - res.status(200).json({ access_token }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}; +const me = await getCurrentUser(client); +console.log(me); -const refreshHandler = async (req, res) => { - try { - const { refresh_token } = req.cookies; - const { access_token } = await authFlow.refresh(refresh_token); - res.status(200).json({ access_token }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}; +const result = await search(client, "track", "Never Gonna Give You Up"); +console.log(result.tracks.items.at(0)); ``` -> It is also recommended to use `state`, which provides protection against -> attacks such as cross-site request forgery, but in the examples below we will -> not use it for simplicity. - -Real code examples with AuthCode flow: - -- [examples/node-express-auth](https://github.com/MellKam/soundify/tree/main/examples/node-express-auth), -- [examples/next-ssr](https://github.com/MellKam/soundify/tree/main/examples/next-ssr), -- [examples/deno-oak-auth](https://github.com/MellKam/soundify/tree/main/examples/deno-oak-auth) - -
+Compared to the usual OOP way of creating API clients, this approach has several +advantages. The main one is that it is _tree-shakable_. You only ship code you +use. This may be not that important for server-side apps, but I'm sure frontend +users will thank you for not including an extra 10kb of crappy js into your +bundle. -
-

Authorization Code flow with PKCE

- -This thread is similar to AuthCode, but it is handled on the client and -therefore does not require SPOTIFY_CLIENT_SECRET. +### Error handling 📛 ```ts -import { PKCEAuthCode } from "@soundify/web-api"; +import { getCurrentUser, SpotifyClient, SpotifyError } from "@soundify/web-api"; -const authFlow = new PKCEAuthCode("YOUR_CLIENT_ID"); +const client = new SpotifyClient("INVALID_ACCESS_TOKEN"); -const authorize = async () => { - const { code_challenge, code_verifier } = await PKCEAuthCode.generateCodes(); - localStorage.setItem("code_verifier", code_verifier); +try { + const me = await getCurrentUser(client); + console.log(me); +} catch (error) { + if (error instanceof SpotifyError) { + error.status; // 401 - location.replace( - authFlow.getAuthURL({ - code_challenge, - scopes: ["user-read-email"], - redirect_uri: "YOUR_REDIRECT_URI" - }) - ); -}; + const message = typeof error.body === "string" + ? error.body + : error.body?.error.message; + console.error(message); // "Invalid access token" -const codeHandler = async () => { - const data = PKCEAuthCode.parseCallbackData( - new URLSearchParams(location.search) - ); - - if ("error" in data) { - throw new Error(data.error); - } - - const code_verifier = localStorage.getItem("code_verifier"); - if (!code_verifier) { - throw new Error("Cannot find code_verifier"); - } - - const { refresh_token, access_token } = authFlow.getGrantData({ - code: data.code, - code_verifier, - redirect_uri: "YOUR_REDIRECT_URI" - }); - - localStorage.removeItem("code_verifier"); - localStorage.setItem("refresh_token", refresh_token); - localStorage.setItem("access_token", access_token); -}; + error.response.headers.get("Date"); // You can access the response here -const refreshHandler = () => { - const refreshToken = localStorage.getItem("refresh_token"); - const { access_token, refresh_token } = authFlow.refresh(refreshToken); + console.error(error); + // SpotifyError: 401 Unauthorized (https://api.spotify.com/v1/me) : Invalid access token + return; + } - localStorage.setItem("refresh_token", refresh_token); - localStorage.setItem("access_token", access_token); -}; + // If it's not a SpotifyError, then it's some type of network error that fetch throws + // Or can be DOMException if you abort the request + console.error("We're totally f#%ked!"); +} ``` -Real code examples with PKCEAuthCode flow: +### Rate Limiting 🕒 -- [examples/react-pkce-auth](https://github.com/MellKam/soundify/tree/main/examples/react-pkce-auth) +If you're really annoying customer, Spotify may block you for some time. To know +what time you need to wait, you can use `Retry-After` header, which will tell +you time in seconds. +[More about rate limiting↗](https://developer.spotify.com/documentation/web-api/concepts/rate-limits) -
- -
-

Client Credentials flow

- -This flow is used in server-to-server authentication. Since this flow does not -include authorization, only endpoints that do not access user information can be -accessed. +To handle this automatically, you can use `waitForRateLimit` option in +`SpotifyClient`. (it's disabled by default, because it may block your code for +unknown time) ```ts -import { ClientCredentials } from "@soundify/web-api"; - -const authFlow = new ClientCredentials({ - client_id: "YOUR_CLIENT_ID", - client_secret: "YOUR_CLIENT_SECRET" +const client = new SpotifyClient("YOUR_ACCESS_TOKEN", { + waitForRateLimit: true, + // wait only if it's less than a minute + waitForRateLimit: (retryAfter) => retryAfter < 60, }); - -const { access_token } = await authFlow.getAccessToken(); ``` -Real code examples with ClientCredentials flow: - -- [examples/deno-client-credentials](https://github.com/MellKam/soundify/tree/main/examples/deno-client-credentials) +### Pagination -
- -
-

Implicit Grant flow

- -The implicit grant flow is carried out on the client side and it does not -involve secret keys. Access tokens issued are short-lived with no refresh token -to extend them when they expire. - -> As from Spotify docs: "The implicit grant flow has some important security -> flaws, thus **we don't recommend using this flow**. If you need to implement -> authorization where storing your client secret is not possible, use -> Authorization code with PKCE instead." +To simplify the process of paginating through the results, we provide a +`PageIterator` class. ```ts -import { ImplicitGrant } from "@soundify/web-api"; - -const authFlow = new ImplicitGrant("YOUR_CLIENT_ID"); - -const authorize = () => { - const state = crypto.randomUUID(); - localStorage.setItem("state", state); +import { getPlaylistTracks, SpotifyClient } from "@soundify/web-api"; +import { PageIterator } from "@soundify/web-api/pagination"; - location.replace( - authFlow.getAuthURL({ - scopes: ["user-read-email"], - state, - redirect_uri: "YOUR_REDIRECT_URI" - }) - ); -}; +const client = new SpotifyClient("YOUR_ACCESS_TOKEN", { + waitForRateLimit: true, +}); -const handleCallback = () => { - const data = ImplicitGrant.parseCallbackData(location.hash); - if ("error" in data) { - throw new Error(data.error); - } +const playlistIter = new PageIterator( + (opts) => getPlaylistTracks(client, "37i9dQZEVXbMDoHDwVN2tF", opts), +); - const storedState = localStorage.getItem("state"); - if (!storedState || !params.state || storedState !== params.state) { - throw new Error("Invalid state"); - } +// iterate over all tracks in the playlist +for await (const track of playlistIter) { + console.log(track); +} - localStorage.removeItem("state"); - localStorage.setItem("access_token", data.access_token); -}; +// or collect all tracks into an array +const allTracks = await playlistIter.collect(); +console.log(allTracks.length); ``` -Real code examples with ImplicitGrant flow: - -- [examples/react-implicit-grant](https://github.com/MellKam/soundify/tree/main/examples/react-implicit-grant) +## Authorization -
+Soundify doesn't provide any tools for authorization, because that would require +to write whole oauth library in here. We have many other battle-tested oauth +solutions, like [oauth4webapi](https://github.com/panva/oauth4webapi) or +[oidc-client-ts](https://github.com/authts/oidc-client-ts). I just don't see a +point in reinventing the wheel 🫤. -## Auth provider and automatic tokens refreshing +Despite this, we have a huge directory of examples, including those for +authorization. +[OAuth2 Examples↗](https://github.com/MellKam/soundify/tree/main/examples/oauth) -As you saw earlier, you can simply pass the Access Token to SpotifyClient. But -after some time (1 hour to be exact), it will expire and you'll need to deal -with it yourself. Somehow get a new Access Token and set it on the client. +### Token Refreshing ```ts -import { SpotifyClient, AuthCode } from "@soundify/web-api"; - -const authFlow = new AuthCode({ ... }); -const client = new SpotifyClient("ACCESS_TOKEN"); -// ... -// Oops, token expires :( +import { getCurrentUser, SpotifyClient } from "@soundify/web-api"; -const { access_token } = await authFlow.refresh("REFRESH_TOKEN"); -// set new token to your client -client.setAuthProvider(access_token); -``` +const refresher = () => { + // This function should return a new access token + // You can use any library you want to refresh the token + // Or even make it yourself, we don't care + return "YOUR_NEW_ACCESS"; +}; -But if you don't want to deal with all that, you can just create an -`AuthProvider` and pass it instead of the Access Token. It will automatically -refresh your token. +const accessToken = await refresher(); +const client = new SpotifyClient(accessToken, { refresher }); -```ts -import { SpotifyClient, AuthCode } from "@soundify/web-api"; +const me = await getCurrentUser(client); +console.log(me); -const authFlow = new AuthCode({ ... }); -const authProvider = authFlow.createAuthProvider("YOUR_REFRESH_TOKEN"); +// wait some time to expire the token ... +// 2000 YEARS LATER 🧽 -const client = new SpotifyClient(authProvider); +const me = await getCurrentUser(client); +// client will receive 401 and call your refresher to get new token +// you don't have to worry about it as long as your refresher is working +console.log(me); ``` -You can create an `AuthProvider` from `AuthCode`, `PKCEAuthCode`, -`ClientCredentials` flows. Implicit grant does not allow to implement such -because you have to refresh the page to get a new token. - -Also you can create your own AuthProvider from `AuthProvider` class. +## Other customizations ```ts -import { AuthProvider } from "@soundify/web-api"; - -const authProvider = new AuthProvider({ - refresher: async () => { - // somehow refresh and get new `access_token` - return { access_token }; - } -}); - -const client = new SpotifyClient(authProvider); -``` - -### Refresh Events - -AuthProvider provides an additional option for callback events that may be -usefull in some cases. +import { SpotifyClient } from "@soundify/web-api"; -```ts -import { AuthProvider } from "@soundify/web-api"; - -const authProvider = new AuthProvider({ - refresher: async () => { - // ... - }, - onRefreshSuccess: ({ access_token }) => { - // do something with new token - // for example, store it in localStorage - localStorage.setItem("access_token", access_token); - }, - onRefreshFailure: (error) => { - // do something with error - // for example, ask user to login again - location.replace(PKCEAuthCode.getAuthURL({ ... })); - }, +const client = new SpotifyClient("YOUR_ACCESS_TOKEN", { + // You can use any fetch implementation you want + // For example, you can use `node-fetch` in node.js + fetch: (input, init) => { + return fetch(input, init); + }, + // You can change the base url of the client + // by default it's "https://api.spotify.com/" + beseUrl: "https://example.com/", + middlewares: [(next) => (url, opts) => { + // You can add your own middleware + // For example, you can add some headers to every request + return next(url, opts); + }], }); ``` -## Auth Scopes - -Scopes are usually used when creating authorization url. Pay attention to them, -because many fields and endpoints may not be available if the correct scopes are -not specified. Read the -[Spotify guide](https://developer.spotify.com/documentation/general/guides/authorization/scopes/) -to learn more. - -In Soundify scopes can be used as strings or with const object `SCOPES`. - -```ts -import { SCOPES, AuthCode } from "@soundify/web-api"; - -AuthCode.getAuthURL({ - scopes: ["user-read-email"], - // or like this - scopes: [SCOPES.USER_READ_EMAIL] - // or use all scopes - scopes: Object.values(SCOPES), -}) -``` - ## Contributors ✨ -All contributions are very welcome ❤️ ([emoji key](https://allcontributors.org/docs/en/emoji-key)) +All contributions are very welcome ❤️ +([emoji key](https://allcontributors.org/docs/en/emoji-key)) diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 896ea35..0000000 --- a/codecov.yml +++ /dev/null @@ -1,6 +0,0 @@ -comment: - layout: "reach, diff, flags, files" - behavior: default - require_changes: false - require_base: no - require_head: yes diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..2f3e486 --- /dev/null +++ b/deno.json @@ -0,0 +1,10 @@ +{ + "imports": { + "zod": "https://deno.land/x/zod@v3.22.4/mod.ts", + "oauth4webapi": "https://deno.land/x/oauth4webapi@v2.8.1/mod.ts", + "oak": "https://deno.land/x/oak@v12.5.0/mod.ts", + "std/": "https://deno.land/std@0.213.0/", + "@soundify/web-api": "./src/mod.ts" + }, + "fmt": { "useTabs": true } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..73f9625 --- /dev/null +++ b/deno.lock @@ -0,0 +1,151 @@ +{ + "version": "3", + "remote": { + "https://deno.land/std@0.188.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.188.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.188.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8", + "https://deno.land/std@0.188.0/bytes/bytes_list.ts": "31d664f4d42fa922066405d0e421c56da89d751886ee77bbe25a88bf0310c9d0", + "https://deno.land/std@0.188.0/bytes/concat.ts": "d26d6f3d7922e6d663dacfcd357563b7bf4a380ce5b9c2bbe0c8586662f25ce2", + "https://deno.land/std@0.188.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", + "https://deno.land/std@0.188.0/bytes/ends_with.ts": "4228811ebc71615d27f065c54b5e815ec1972538772b0f413c0efe05245b472e", + "https://deno.land/std@0.188.0/bytes/equals.ts": "b87494ce5442dc786db46f91378100028c402f83a14a2f7bbff6bda7810aefe3", + "https://deno.land/std@0.188.0/bytes/includes_needle.ts": "76a8163126fb2f8bf86fd7f22192c3bb04bf6a20b987a095127c2ca08adf3ba6", + "https://deno.land/std@0.188.0/bytes/index_of_needle.ts": "65c939607df609374c4415598fa4dad04a2f14c4d98cd15775216f0aaf597f24", + "https://deno.land/std@0.188.0/bytes/last_index_of_needle.ts": "7181072883cb4908c6ce8f7a5bb1d96787eef2c2ab3aa94fe4268ab326a53cbf", + "https://deno.land/std@0.188.0/bytes/mod.ts": "e869bba1e7a2e3a9cc6c2d55471888429a544e70a840c087672e656e7ba21815", + "https://deno.land/std@0.188.0/bytes/repeat.ts": "6f5e490d8d72bcbf8d84a6bb04690b9b3eb5822c5a11687bca73a2318a842294", + "https://deno.land/std@0.188.0/bytes/starts_with.ts": "3e607a70c9c09f5140b7a7f17a695221abcc7244d20af3eb47ccbb63f5885135", + "https://deno.land/std@0.188.0/crypto/keystack.ts": "877ab0f19eb7d37ad6495190d3c3e39f58e9c52e0b6a966f82fd6df67ca55f90", + "https://deno.land/std@0.188.0/crypto/timing_safe_equal.ts": "0fae34ee02264f309ae0b6e54e9746a7aba3996e5454903ed106967a7a9ef665", + "https://deno.land/std@0.188.0/encoding/base64.ts": "144ae6234c1fbe5b68666c711dc15b1e9ee2aef6d42b3b4345bf9a6c91d70d0d", + "https://deno.land/std@0.188.0/encoding/base64url.ts": "2ed4ba122b20fedf226c5d337cf22ee2024fa73a8f85d915d442af7e9ce1fae1", + "https://deno.land/std@0.188.0/http/_negotiation/common.ts": "14d1a52427ab258a4b7161cd80e1d8a207b7cc64b46e911780f57ead5f4323c6", + "https://deno.land/std@0.188.0/http/_negotiation/encoding.ts": "ff747d107277c88cb7a6a62a08eeb8d56dad91564cbcccb30694d5dc126dcc53", + "https://deno.land/std@0.188.0/http/_negotiation/language.ts": "7bcddd8db3330bdb7ce4fc00a213c5547c1968139864201efd67ef2d0d51887d", + "https://deno.land/std@0.188.0/http/_negotiation/media_type.ts": "58847517cd549384ad677c0fe89e0a4815be36fe7a303ea63cee5f6a1d7e1692", + "https://deno.land/std@0.188.0/http/cookie_map.ts": "d148a5eaf35f19905dd5104126fa47ac71105306dd42f129732365e43108b28a", + "https://deno.land/std@0.188.0/http/etag.ts": "6ad8abbbb1045aabf2307959a2c5565054a8bf01c9824ddee836b1ff22706a58", + "https://deno.land/std@0.188.0/http/http_errors.ts": "b9a18ef97d6c5966964de95e04d1f9f88a0f8bd8577c26fd402d9d632fb03a42", + "https://deno.land/std@0.188.0/http/http_status.ts": "8a7bcfe3ac025199ad804075385e57f63d055b2aed539d943ccc277616d6f932", + "https://deno.land/std@0.188.0/http/method.ts": "e66c2a015cb46c21ab0bb3589aa4fca43143a506cb324ffdfd42d2edef7bc0c4", + "https://deno.land/std@0.188.0/http/negotiation.ts": "46e74a6bad4b857333a58dc5b50fe8e5a4d5267e97292293ea65f980bd918086", + "https://deno.land/std@0.188.0/http/server_sent_event.ts": "f752c9e4f0eba247a3fead8bfe0883f44e04d31c2dcc88776ba5230b7165a460", + "https://deno.land/std@0.188.0/io/buf_reader.ts": "06fff3337091c49e99ebd2dd790c9a90364c087a2953ea081667400fd6c6cebb", + "https://deno.land/std@0.188.0/io/buf_writer.ts": "48c33c8f00b61dcbc7958706741cec8e59810bd307bc6a326cbd474fe8346dfd", + "https://deno.land/std@0.188.0/io/buffer.ts": "17f4410eaaa60a8a85733e8891349a619eadfbbe42e2f319283ce2b8f29723ab", + "https://deno.land/std@0.188.0/io/copy_n.ts": "0cc7ce07c75130f6fc18621ec1911c36e147eb9570664fee0ea12b1988167590", + "https://deno.land/std@0.188.0/io/limited_reader.ts": "6c9a216f8eef39c1ee2a6b37a29372c8fc63455b2eeb91f06d9646f8f759fc8b", + "https://deno.land/std@0.188.0/io/mod.ts": "2665bcccc1fd6e8627cca167c3e92aaecbd9897556b6f69e6d258070ef63fd9b", + "https://deno.land/std@0.188.0/io/multi_reader.ts": "9c2a0a31686c44b277e16da1d97b4686a986edcee48409b84be25eedbc39b271", + "https://deno.land/std@0.188.0/io/read_delim.ts": "c02b93cc546ae8caad8682ae270863e7ace6daec24c1eddd6faabc95a9d876a3", + "https://deno.land/std@0.188.0/io/read_int.ts": "7cb8bcdfaf1107586c3bacc583d11c64c060196cb070bb13ae8c2061404f911f", + "https://deno.land/std@0.188.0/io/read_lines.ts": "c526c12a20a9386dc910d500f9cdea43cba974e853397790bd146817a7eef8cc", + "https://deno.land/std@0.188.0/io/read_long.ts": "f0aaa420e3da1261c5d33c5e729f09922f3d9fa49f046258d4ff7a00d800c71e", + "https://deno.land/std@0.188.0/io/read_range.ts": "28152daf32e43dd9f7d41d8466852b0d18ad766cd5c4334c91fef6e1b3a74eb5", + "https://deno.land/std@0.188.0/io/read_short.ts": "805cb329574b850b84bf14a92c052c59b5977a492cd780c41df8ad40826c1a20", + "https://deno.land/std@0.188.0/io/read_string_delim.ts": "5dc9f53bdf78e7d4ee1e56b9b60352238ab236a71c3e3b2a713c3d78472a53ce", + "https://deno.land/std@0.188.0/io/slice_long_to_bytes.ts": "48d9bace92684e880e46aa4a2520fc3867f9d7ce212055f76ecc11b22f9644b7", + "https://deno.land/std@0.188.0/io/string_reader.ts": "da0f68251b3d5b5112485dfd4d1b1936135c9b4d921182a7edaf47f74c25cc8f", + "https://deno.land/std@0.188.0/io/string_writer.ts": "8a03c5858c24965a54c6538bed15f32a7c72f5704a12bda56f83a40e28e5433e", + "https://deno.land/std@0.188.0/media_types/_db.ts": "7606d83e31f23ce1a7968cbaee852810c2cf477903a095696cdc62eaab7ce570", + "https://deno.land/std@0.188.0/media_types/_util.ts": "916efbd30b6148a716f110e67a4db29d6949bf4048997b754415dd7e42c52378", + "https://deno.land/std@0.188.0/media_types/content_type.ts": "ad98a5aa2d95f5965b2796072284258710a25e520952376ed432b0937ce743bc", + "https://deno.land/std@0.188.0/media_types/extension.ts": "a7cd28c9417143387cdfed27d4e8607ebcf5b1ec27eb8473d5b000144689fe65", + "https://deno.land/std@0.188.0/media_types/extensions_by_type.ts": "43806d6a52a0d6d965ada9d20e60a982feb40bc7a82268178d94edb764694fed", + "https://deno.land/std@0.188.0/media_types/format_media_type.ts": "f5e1073c05526a6f5a516ac5c5587a1abd043bf1039c71cde1166aa4328c8baf", + "https://deno.land/std@0.188.0/media_types/get_charset.ts": "18b88274796fda5d353806bf409eb1d2ddb3f004eb4bd311662c4cdd8ac173db", + "https://deno.land/std@0.188.0/media_types/mod.ts": "d3f0b99f85053bc0b98ecc24eaa3546dfa09b856dc0bbaf60d8956d2cdd710c8", + "https://deno.land/std@0.188.0/media_types/parse_media_type.ts": "835c4112e1357e95b4f10d7cdea5ae1801967e444f48673ff8f1cb4d32af9920", + "https://deno.land/std@0.188.0/media_types/type_by_extension.ts": "daa801eb0f11cdf199445d0f1b656cf116d47dcf9e5b85cc1e6b4469f5ee0432", + "https://deno.land/std@0.188.0/media_types/vendor/mime-db.v1.52.0.ts": "6925bbcae81ca37241e3f55908d0505724358cda3384eaea707773b2c7e99586", + "https://deno.land/std@0.188.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.188.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.188.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.188.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.188.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.188.0/path/mod.ts": "ee161baec5ded6510ee1d1fb6a75a0f5e4b41f3f3301c92c716ecbdf7dae910d", + "https://deno.land/std@0.188.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.188.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.188.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", + "https://deno.land/std@0.188.0/streams/_common.ts": "f45cba84f0d813de3326466095539602364a9ba521f804cc758f7a475cda692d", + "https://deno.land/std@0.188.0/streams/buffer.ts": "d5b3d7d0299114e5b2ea895a8bf202a687fd915c5282f8096c7bae23b5a04407", + "https://deno.land/std@0.188.0/streams/byte_slice_stream.ts": "225d57263a34325d7c96cb3dafeb478eec0e6fd05cd0458d678752eadd132bb4", + "https://deno.land/std@0.188.0/streams/copy.ts": "75cbc795ff89291df22ddca5252de88b2e16d40c85d02840593386a8a1454f71", + "https://deno.land/std@0.188.0/streams/delimiter_stream.ts": "f69e849b3d1f59f02424497273f411105a6f76a9f13da92aeeb9a2d554236814", + "https://deno.land/std@0.188.0/streams/early_zip_readable_streams.ts": "4005fa74162b943f79899e5d7cb96adcbc0a6b867f9144974ed12d30e0a556e1", + "https://deno.land/std@0.188.0/streams/iterate_reader.ts": "bbec1d45c2df2c0c5920bad0549351446fdc8e0886d99e95959b259dbcdb6072", + "https://deno.land/std@0.188.0/streams/limited_bytes_transform_stream.ts": "05dc592ffaab83257494d22dd53917e56243c26e5e3129b3f13ddbbbc4785048", + "https://deno.land/std@0.188.0/streams/limited_transform_stream.ts": "d69ab790232c1b86f53621ad41ef03c235f2abb4b7a1cd51960ad6e12ee55e38", + "https://deno.land/std@0.188.0/streams/merge_readable_streams.ts": "5d6302888f4bb0616dafb5768771be0aec9bedc05fbae6b3d726d05ffbec5b15", + "https://deno.land/std@0.188.0/streams/mod.ts": "c07ec010e700b9ea887dc36ca08729828bc7912f711e4054e24d33fd46282252", + "https://deno.land/std@0.188.0/streams/read_all.ts": "ee319772fb0fd28302f97343cc48dfcf948f154fd0d755d8efe65814b70533be", + "https://deno.land/std@0.188.0/streams/readable_stream_from_iterable.ts": "cd4bb9e9bf6dbe84c213beb1f5085c326624421671473e410cfaecad15f01865", + "https://deno.land/std@0.188.0/streams/readable_stream_from_reader.ts": "bfc416c4576a30aac6b9af22c9dc292c20c6742141ee7c55b5e85460beb0c54e", + "https://deno.land/std@0.188.0/streams/reader_from_iterable.ts": "55f68110dce3f8f2c87b834d95f153bc904257fc65175f9f2abe78455cb8047c", + "https://deno.land/std@0.188.0/streams/reader_from_stream_reader.ts": "fa4971e5615a010e49492c5d1688ca1a4d17472a41e98b498ab89a64ebd7ac73", + "https://deno.land/std@0.188.0/streams/text_delimiter_stream.ts": "20e680ab8b751390e359288ce764f9c47d164af11a263870746eeca4bc7d976b", + "https://deno.land/std@0.188.0/streams/text_line_stream.ts": "0f2c4b33a5fdb2476f2e060974cba1347cefe99a4af33c28a57524b1a34750fa", + "https://deno.land/std@0.188.0/streams/to_transform_stream.ts": "7f55fc0b14cf3ed0f8d10d8f41d05bdc40726e44a65c37f58705d10a615f0159", + "https://deno.land/std@0.188.0/streams/writable_stream_from_writer.ts": "56fff5c82fb736fdd669b567cc0b2bbbe0351002cd13254eae26c366e2bed89a", + "https://deno.land/std@0.188.0/streams/write_all.ts": "aec90152978581ea62d56bb53a5cbf487e6a89c902f87c5969681ffbdf32b998", + "https://deno.land/std@0.188.0/streams/writer_from_stream_writer.ts": "07c7ee025151a190f37fc42cbb01ff93afc949119ebddc6e0d0df14df1bf6950", + "https://deno.land/std@0.188.0/streams/zip_readable_streams.ts": "a9d81aa451240f79230add674809dbee038d93aabe286e2d9671e66591fc86ca", + "https://deno.land/std@0.213.0/dotenv/mod.ts": "46cc7a78cf3f7ba533c8c9b90407a365f925be332ebdc867a1f502614e3d8523", + "https://deno.land/std@0.213.0/dotenv/parse.ts": "55d5d1a5ba76c1324e6dc2f2f7917f03c47e5da5c2c55af11fd0fe2d6a3d3a2e", + "https://deno.land/std@0.213.0/dotenv/stringify.ts": "0047ad7068289735d08964046aea267a750c141b494ca0e38831b89be6c020c2", + "https://deno.land/std@0.213.0/encoding/_util.ts": "beacef316c1255da9bc8e95afb1fa56ed69baef919c88dc06ae6cb7a6103d376", + "https://deno.land/std@0.213.0/encoding/base64.ts": "0ec6d6e6b68fc38f6396277e5184bcd47c1a9db0222fd0b563487eb67e352741", + "https://deno.land/std@0.213.0/encoding/base64url.ts": "b6e2d187e425e27227d6162e297c2f356dd8f23d4ddca21d0850e6871fe8ef37", + "https://deno.land/x/oak@v12.5.0/application.ts": "3028d3f6fa5ee743de013881550d054372c11d83c45099c2d794033786d27008", + "https://deno.land/x/oak@v12.5.0/body.ts": "1899761b97fc9d776f3710b2637fb047ba29b968609afc6c0e5219b1108e703c", + "https://deno.land/x/oak@v12.5.0/buf_reader.ts": "26640736541598dbd9f2b84a9d0595756afff03c9ca55b66eef1911f7798b56d", + "https://deno.land/x/oak@v12.5.0/content_disposition.ts": "8b8c3cb2fba7138cd5b7f82fc3b5ea39b33db924a824b28261659db7e164621e", + "https://deno.land/x/oak@v12.5.0/context.ts": "895a2d40186b89c28ba3947bf08a9335f1a11fc33133f760082536b74c53d1ca", + "https://deno.land/x/oak@v12.5.0/deps.ts": "047cba410eec1da9e1d4b46d634a13b4fcb4bc643bf9593e2755077d2a5e5187", + "https://deno.land/x/oak@v12.5.0/etag.ts": "32e47726b41698aefdd71faac5aaf2907c9bdd41ef18a7693863be4f8fee115d", + "https://deno.land/x/oak@v12.5.0/forwarded.ts": "e656f96a85574e2a6ee54dc35efc9f72d543c9ae0923760ef426ee7369eef01c", + "https://deno.land/x/oak@v12.5.0/headers.ts": "769fd042d34fbcd5667cbd745b5c65d335cc8430e822dbf1f87d65313cab4b47", + "https://deno.land/x/oak@v12.5.0/helpers.ts": "6b03c6a2be06ec775d54449e442a2bac234aa952948ca758356eab6dc87af618", + "https://deno.land/x/oak@v12.5.0/http_server_native.ts": "6232bed3d7bc17136d1a7f7685a6f8a79c943694463f71cf743eac58e75ec34c", + "https://deno.land/x/oak@v12.5.0/http_server_native_request.ts": "552b174b5e13e92de8897d5b6f716b1e5a53543115481d65a651a41e4ca29ec9", + "https://deno.land/x/oak@v12.5.0/isMediaType.ts": "62d638abcf837ece3a8f07a4b7ca59794135cb0d4b73194c7d5837efd4161005", + "https://deno.land/x/oak@v12.5.0/mediaTyper.ts": "042b853fc8e9c3f6c628dd389e03ef481552bf07242efc3f8a1af042102a6105", + "https://deno.land/x/oak@v12.5.0/middleware.ts": "c7f7a0424a6dd99a00e4b8d7d6e131efc0facc8dea781845d713b63df8ef1862", + "https://deno.land/x/oak@v12.5.0/middleware/proxy.ts": "6f2799cf60d926e7a8d13ff757a59d7f0f930407db7ee9b81e7c064138eb89ff", + "https://deno.land/x/oak@v12.5.0/mod.ts": "f6aa47ad1b6099470c9a884cccad9d3ac0fd242ba940896291ab76cd26cf554b", + "https://deno.land/x/oak@v12.5.0/multipart.ts": "1484e01b98f5135f2aa09f7d0ce1e7be39109bf9f045ac660e941619d04e3d29", + "https://deno.land/x/oak@v12.5.0/range.ts": "1ca15fc1ac21c650c34e6997a75af2af9d9d8eb6fe2d5d1dadeac3dfd4a9c152", + "https://deno.land/x/oak@v12.5.0/request.ts": "b7f4f5c138c3feacd318ed333e3d3c0b5d37555b131ce38a2a8bb28415aab8d1", + "https://deno.land/x/oak@v12.5.0/response.ts": "7172285e58947057f36d6536b52a9cdffbdb03616555fa8e94b87f469a105f8a", + "https://deno.land/x/oak@v12.5.0/router.ts": "0f53d6249f9e8f89f2522b2b810b9302d0f22593c184b16b24b03bf2b7d42ea1", + "https://deno.land/x/oak@v12.5.0/send.ts": "5ec49f106294593f468317a0c885da4f3274bf6d0fe9e16a7304391730b4f4fb", + "https://deno.land/x/oak@v12.5.0/structured_clone.ts": "c3888b14d1eec558345bfbf13d0993d59bd45aaa8588444e35dd558c3a921cd8", + "https://deno.land/x/oak@v12.5.0/testing.ts": "37d684d57bb8e5150fb5eb2677e66b04dcb422709cf2c5a74c1df94d52aa02e2", + "https://deno.land/x/oak@v12.5.0/util.ts": "0a3fdffb114859c2de84e1783efa3a544af4d2af8c6f08e0d25655de9d3e69bb", + "https://deno.land/x/oauth4webapi@v2.8.1/mod.ts": "cac8e55ad9db1ea74f90c1a39bfecee793dc4a02faba5f75a31ca64804ec5d68", + "https://deno.land/x/oauth4webapi@v2.8.1/src/index.ts": "bf667804a2faa04818067ff1f871bad6129c93c994395c10fd31bfc25c7bce11", + "https://deno.land/x/path_to_regexp@v6.2.1/index.ts": "894060567837bae8fc9c5cbd4d0a05e9024672083d5883b525c031eea940e556", + "https://deno.land/x/zod@v3.22.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea", + "https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef", + "https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe", + "https://deno.land/x/zod@v3.22.4/helpers/enumUtil.ts": "54efc393cc9860e687d8b81ff52e980def00fa67377ad0bf8b3104f8a5bf698c", + "https://deno.land/x/zod@v3.22.4/helpers/errorUtil.ts": "7a77328240be7b847af6de9189963bd9f79cab32bbc61502a9db4fe6683e2ea7", + "https://deno.land/x/zod@v3.22.4/helpers/parseUtil.ts": "f791e6e65a0340d85ad37d26cd7a3ba67126cd9957eac2b7163162155283abb1", + "https://deno.land/x/zod@v3.22.4/helpers/partialUtil.ts": "998c2fe79795257d4d1cf10361e74492f3b7d852f61057c7c08ac0a46488b7e7", + "https://deno.land/x/zod@v3.22.4/helpers/typeAliases.ts": "0fda31a063c6736fc3cf9090dd94865c811dfff4f3cb8707b932bf937c6f2c3e", + "https://deno.land/x/zod@v3.22.4/helpers/util.ts": "8baf19b19b2fca8424380367b90364b32503b6b71780269a6e3e67700bb02774", + "https://deno.land/x/zod@v3.22.4/index.ts": "d27aabd973613985574bc31f39e45cb5d856aa122ef094a9f38a463b8ef1a268", + "https://deno.land/x/zod@v3.22.4/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c", + "https://deno.land/x/zod@v3.22.4/mod.ts": "64e55237cb4410e17d968cd08975566059f27638ebb0b86048031b987ba251c4", + "https://deno.land/x/zod@v3.22.4/types.ts": "724185522fafe43ee56a52333958764c8c8cd6ad4effa27b42651df873fc151e" + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:all-contributors-cli@6.26.1", + "npm:tsup@8.0.1", + "npm:typescript@5.3.3" + ] + } + } +} diff --git a/examples/next-ssr/.env.example b/examples/next-ssr/.env.example deleted file mode 100644 index a565b92..0000000 --- a/examples/next-ssr/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -SPOTIFY_CLIENT_ID= -SPOTIFY_CLIENT_SECRET= -SPOTIFY_REDIRECT_URI=http://localhost:3000/api/callback \ No newline at end of file diff --git a/examples/next-ssr/.gitignore b/examples/next-ssr/.gitignore deleted file mode 100644 index c87c9b3..0000000 --- a/examples/next-ssr/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/examples/next-ssr/README.md b/examples/next-ssr/README.md deleted file mode 100644 index f3f1c6e..0000000 --- a/examples/next-ssr/README.md +++ /dev/null @@ -1,5 +0,0 @@ -Soundify example banner - -# Soundify exapmle (next-ssr) - -// TODO diff --git a/examples/next-ssr/next.config.js b/examples/next-ssr/next.config.js deleted file mode 100644 index 6d32668..0000000 --- a/examples/next-ssr/next.config.js +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true -}; - -module.exports = nextConfig; diff --git a/examples/next-ssr/package.json b/examples/next-ssr/package.json deleted file mode 100644 index f25eb26..0000000 --- a/examples/next-ssr/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@examples/next-ssr", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@types/node": "latest", - "@types/react": "latest", - "@types/react-dom": "latest", - "cookies-next": "2.1.2", - "next": "13.4.6", - "react": "18.2.0", - "react-dom": "18.2.0", - "@soundify/web-api": "workspace:*", - "@tanstack/react-query": "4.29.14", - "typescript": "5.1.3" - } -} diff --git a/examples/next-ssr/pages/_app.tsx b/examples/next-ssr/pages/_app.tsx deleted file mode 100644 index 645c5f0..0000000 --- a/examples/next-ssr/pages/_app.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import type { AppProps } from "next/app"; - -const queryClient = new QueryClient(); - -export default function App({ Component, pageProps }: AppProps) { - return ( - - - - ); -} diff --git a/examples/next-ssr/pages/_document.tsx b/examples/next-ssr/pages/_document.tsx deleted file mode 100644 index ffc3f3c..0000000 --- a/examples/next-ssr/pages/_document.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Head, Html, Main, NextScript } from "next/document"; - -export default function Document() { - return ( - - - -
- - - - ); -} diff --git a/examples/next-ssr/pages/api/auth.ts b/examples/next-ssr/pages/api/auth.ts deleted file mode 100644 index 83bef72..0000000 --- a/examples/next-ssr/pages/api/auth.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { randomUUID } from "node:crypto"; -import { setCookie } from "cookies-next"; -import { authFlow, env, STATE } from "../../spotify"; - -export default function (req: NextApiRequest, res: NextApiResponse) { - const state = randomUUID(); - - setCookie(STATE, state, { - httpOnly: true, - path: "/api/callback", - req, - res - }); - - res.redirect( - authFlow - .getAuthURL({ - scopes: ["user-read-email"], - state, - redirect_uri: env.redirect_uri - }) - .toString() - ); -} diff --git a/examples/next-ssr/pages/api/callback.ts b/examples/next-ssr/pages/api/callback.ts deleted file mode 100644 index a71f9eb..0000000 --- a/examples/next-ssr/pages/api/callback.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { deleteCookie, getCookie, setCookie } from "cookies-next"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { - ACCESS_TOKEN, - authFlow, - env, - REFRESH_TOKEN, - STATE -} from "../../spotify"; -import { AuthCodeFlow } from "@soundify/web-api"; - -export default async function (req: NextApiRequest, res: NextApiResponse) { - if (!req.url) { - res.status(400).send("req.url is undefined"); - return; - } - - const url = new URL(req.url, `http://${req.headers.host}`); - - const data = AuthCodeFlow.parseCallbackData(url.searchParams); - - if ("error" in data) { - res.status(400).send(data.error); - return; - } - - const storedState = getCookie(STATE, { req, res }); - if ( - typeof data.state !== "string" || - !storedState || - data.state !== storedState - ) { - res.status(400).send("Unable to verify request with state."); - return; - } - - deleteCookie(STATE, { req, res }); - - try { - const { refresh_token, access_token, expires_in } = - await authFlow.getGrantData(env.redirect_uri, data.code); - - setCookie(REFRESH_TOKEN, refresh_token, { - httpOnly: true, - sameSite: "strict", - path: "/api/refresh", - req, - res - }); - - setCookie(ACCESS_TOKEN, access_token, { - req, - res, - maxAge: expires_in, - sameSite: "strict" - }); - - res.redirect("/"); - } catch (error) { - res.status(400).send(String(error)); - } -} diff --git a/examples/next-ssr/pages/api/refresh.ts b/examples/next-ssr/pages/api/refresh.ts deleted file mode 100644 index fc5f605..0000000 --- a/examples/next-ssr/pages/api/refresh.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { getCookie, setCookie } from "cookies-next"; -import { ACCESS_TOKEN, authFlow, REFRESH_TOKEN } from "../../spotify"; - -export default async function (req: NextApiRequest, res: NextApiResponse) { - const refresh_token = getCookie(REFRESH_TOKEN, { - req, - res - }); - - if (typeof refresh_token !== "string") { - res.status(400).send("Can't find REFRESH_TOKEN"); - return; - } - - try { - const { access_token, expires_in } = await authFlow.refresh(refresh_token); - - setCookie(ACCESS_TOKEN, access_token, { - maxAge: expires_in, - req, - res, - sameSite: "strict" - }); - - res.status(204).end(); - } catch (error) { - res.status(500).send({ error: String(error) }); - } -} diff --git a/examples/next-ssr/pages/index.tsx b/examples/next-ssr/pages/index.tsx deleted file mode 100644 index e3e75d1..0000000 --- a/examples/next-ssr/pages/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { getCookie } from "cookies-next"; -import { useCallback, useMemo } from "react"; -import { getCurrentUser, SpotifyClient } from "@soundify/web-api"; -import { ACCESS_TOKEN } from "../spotify"; -import { useQuery } from "@tanstack/react-query"; - -export default function () { - const authorize = useCallback(() => location.replace("/api/auth"), []); - - const client = useMemo(() => { - const accessToken = getCookie(ACCESS_TOKEN); - - return new SpotifyClient( - { - token: typeof accessToken === "string" ? accessToken : undefined, - refresh: async () => { - const res = await fetch("/api/refresh"); - if (!res.ok) { - throw new Error(await res.text()); - } - - const access_token = getCookie(ACCESS_TOKEN); - if (typeof access_token !== "string") { - throw new Error("Cannot refresh access token"); - } - - return access_token; - } - }, - { - onUnauthorized: authorize - } - ); - }, []); - - const { - data: user, - status, - error - } = useQuery({ - queryKey: ["user-profile"], - queryFn: () => getCurrentUser(client), - retry: false - }); - - if (status === "error") { - return

{String(error)}

; - } - - if (status === "loading") { - return

Loading...

; - } - - return

Welcome {user.display_name}!

; -} diff --git a/examples/next-ssr/spotify.ts b/examples/next-ssr/spotify.ts deleted file mode 100644 index 9167b50..0000000 --- a/examples/next-ssr/spotify.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AuthCodeFlow } from "@soundify/web-api"; - -/** Names for cookies */ -export const ACCESS_TOKEN = "SPOTIFY_ACCESS_TOKEN"; -export const REFRESH_TOKEN = "SPOTIFY_REFRESH_TOKE"; -export const STATE = "SPOTIFY_STATE"; - -export const env = { - client_id: process.env.SPOTIFY_CLIENT_ID!, - client_secret: process.env.SPOTIFY_CLIENT_SECRET!, - redirect_uri: process.env.SPOTIFY_REDIRECT_URI! -}; - -export const authFlow = new AuthCodeFlow({ - client_id: env.client_id, - client_secret: env.client_secret -}); diff --git a/examples/next-ssr/tsconfig.json b/examples/next-ssr/tsconfig.json deleted file mode 100644 index f4ab65f..0000000 --- a/examples/next-ssr/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "baseUrl": ".", - "paths": { - "@/*": ["./*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] -} diff --git a/examples/node-express-auth/.env.example b/examples/node-express-auth/.env.example deleted file mode 100644 index ea1a2a9..0000000 --- a/examples/node-express-auth/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -SPOTIFY_CLIENT_ID= -SPOTIFY_CLIENT_SECRET= -SPOTIFY_REDIRECT_URI= \ No newline at end of file diff --git a/examples/node-express-auth/README.md b/examples/node-express-auth/README.md deleted file mode 100644 index e09f5a2..0000000 --- a/examples/node-express-auth/README.md +++ /dev/null @@ -1,5 +0,0 @@ -Soundify example banner - -# Soundify exapmle (node-express-auth) - -// TODO diff --git a/examples/node-express-auth/package.json b/examples/node-express-auth/package.json deleted file mode 100644 index 602b4c4..0000000 --- a/examples/node-express-auth/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@examples/node-express-auth", - "private": "true", - "type": "module", - "scripts": { - "dev": "tsc && node ./dist/main.js" - }, - "devDependencies": { - "@types/cookie-parser": "1.4.3", - "@types/express": "4.17.17", - "@types/node": "latest", - "typescript": "5.1.3" - }, - "dependencies": { - "cookie-parser": "1.4.6", - "express": "4.18.2", - "@soundify/web-api": "workspace:*" - } -} diff --git a/examples/node-express-auth/src/main.ts b/examples/node-express-auth/src/main.ts deleted file mode 100644 index 3be2081..0000000 --- a/examples/node-express-auth/src/main.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { AuthCodeFlow } from "@soundify/web-api"; -import { randomUUID } from "node:crypto"; -import express from "express"; -import cookieParser from "cookie-parser"; - -const app = express(); -app.use(cookieParser("secret")); - -const env = { - client_id: process.env.SPOTIFY_CLIENT_ID!, - client_secret: process.env.SPOTIFY_CLIENT_SECRET!, - redirect_uri: process.env.SPOTIFY_REDIRECT_URI! -}; - -const authFlow = new AuthCodeFlow({ - client_id: env.client_id, - client_secret: env.client_secret -}); - -app.get("/login", (_, res) => { - const state = randomUUID(); - - res.cookie("state", state, { - httpOnly: true - }); - res.redirect( - authFlow - .getAuthURL({ - scopes: ["user-read-email"], - state, - redirect_uri: env.redirect_uri - }) - .toString() - ); -}); - -app.get("/callback", async (req, res) => { - try { - const searchParams = new URL(req.url, `http://${req.headers.host}`) - .searchParams; - const data = AuthCodeFlow.parseCallbackData(searchParams); - if ("error" in data) { - throw new Error(data.error); - } - - const storedState = req.cookies["state"]; - if (!storedState || !data.state || storedState !== data.state) { - throw new Error("Unable to verify request with state."); - } - - const grantData = await authFlow.getGrantData(env.redirect_uri, data.code); - - res.status(200).json(grantData); - } catch (error) { - res.status(400).send(String(error)); - } -}); - -const port = process.env.PORT; -app.listen(port, () => { - console.log( - `To get OAuth tokens, navigate to: http://localhost:${port}/login` - ); -}); diff --git a/examples/node-express-auth/tsconfig.json b/examples/node-express-auth/tsconfig.json deleted file mode 100644 index 8baaccc..0000000 --- a/examples/node-express-auth/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "forceConsistentCasingInFileNames": true, - "allowSyntheticDefaultImports": true, - "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, - "noUnusedParameters": true, - "strict": true, - "declaration": true, - "moduleResolution": "node", - "module": "ESNext", - "rootDir": "./src/", - "outDir": "./dist/", - "target": "ESNext" - } -} diff --git a/examples/oauth/auth_code.ts b/examples/oauth/auth_code.ts new file mode 100644 index 0000000..a2d46cb --- /dev/null +++ b/examples/oauth/auth_code.ts @@ -0,0 +1,165 @@ +import * as oauth from "oauth4webapi"; +import { OAUTH_SCOPES, SPOTIFY_AUTH_URL } from "@soundify/web-api"; +import { z } from "zod"; +import { load } from "std/dotenv/mod.ts"; +import { encodeBase64Url } from "std/encoding/base64url.ts"; +import { Application, Router } from "oak"; + +await load({ export: true }); + +const env = z + .object({ + SPOTIFY_CLIENT_ID: z.string(), + SPOTIFY_CLIENT_SECRET: z.string(), + SPOTIFY_REDIRECT_URI: z.string().url(), + }) + .parse(Deno.env.toObject()); + +const issuer = new URL(SPOTIFY_AUTH_URL); +const authServer = await oauth.processDiscoveryResponse( + issuer, + await oauth.discoveryRequest(issuer), +); + +const oauthClient: oauth.Client = { + client_id: env.SPOTIFY_CLIENT_ID, + client_secret: env.SPOTIFY_CLIENT_SECRET, + token_endpoint_auth_method: "client_secret_basic", +}; + +const app = new Application(); +const router = new Router(); + +router.get("/login", async (ctx) => { + const state = oauth.generateRandomState(); + await ctx.cookies.set("state", state, { + httpOnly: true, + path: "/callback", + }); + + const authUrl = new URL(authServer.authorization_endpoint!); + authUrl.searchParams.set("client_id", env.SPOTIFY_CLIENT_ID); + authUrl.searchParams.set("redirect_uri", env.SPOTIFY_REDIRECT_URI); + authUrl.searchParams.set("response_type", "code"); + authUrl.searchParams.set("state", state); + authUrl.searchParams.set("scope", Object.values(OAUTH_SCOPES).join(" ")); + + return ctx.response.redirect(authUrl); +}); + +router.get("/callback", async (ctx) => { + try { + const state = await ctx.cookies.get("state"); + if (!state) { + ctx.response.status = 400; + ctx.response.body = "Missing state cookie"; + return; + } + + const params = oauth.validateAuthResponse( + authServer, + oauthClient, + ctx.request.url, + state, + ); + if (oauth.isOAuth2Error(params)) { + throw new Error( + params.error + params.error_description + ? " : " + params.error_description + : "", + ); + } + + const response = await fetch(new URL("/api/token", SPOTIFY_AUTH_URL), { + method: "POST", + body: new URLSearchParams({ + grant_type: "authorization_code", + redirect_uri: env.SPOTIFY_REDIRECT_URI, + code: params.get("code")!, + }), + headers: { + Authorization: "Basic " + + encodeBase64Url( + env.SPOTIFY_CLIENT_ID + ":" + env.SPOTIFY_CLIENT_SECRET, + ), + }, + }); + + const result = await oauth.processAuthorizationCodeOAuth2Response( + authServer, + oauthClient, + response, + ); + + if (oauth.isOAuth2Error(result)) { + throw new Error( + result.error + result.error_description + ? " : " + result.error_description + : "", + ); + } + + if (!result.refresh_token) { + throw new Error("Missing refresh token"); + } + + await ctx.cookies.set("refresh_token", result.refresh_token, { + httpOnly: true, + path: "/refresh", + }); + + ctx.response.type = "application/json"; + ctx.response.body = JSON.stringify(result); + ctx.response.status = 200; + } catch (error) { + console.log(error); + ctx.response.status = 500; + ctx.response.body = error.message; + } finally { + await ctx.cookies.set("state", null, { + httpOnly: true, + path: "/callback", + }); + } +}); + +router.get("/refresh", async (ctx) => { + const refreshToken = await ctx.cookies.get("refresh_token"); + if (!refreshToken) { + ctx.response.status = 400; + ctx.response.body = "Missing refresh token"; + return; + } + + const res = await oauth.refreshTokenGrantRequest( + authServer, + oauthClient, + refreshToken, + ); + const result = await oauth.processRefreshTokenResponse( + authServer, + oauthClient, + res, + ); + + if (oauth.isOAuth2Error(result)) { + ctx.response.status = 500; + ctx.response.body = result.error + result.error_description + ? " : " + result.error_description + : ""; + return; + } + + ctx.response.type = "application/json"; + ctx.response.body = JSON.stringify(result); + ctx.response.status = 200; +}); + +app.use(router.routes()); +app.use(router.allowedMethods()); + +app.addEventListener( + "listen", + (event) => console.log(`http://${event.hostname}:${event.port}/login`), +); +await app.listen({ port: 3000 }); diff --git a/examples/oauth/client_credentials.ts b/examples/oauth/client_credentials.ts new file mode 100644 index 0000000..c3b5b3c --- /dev/null +++ b/examples/oauth/client_credentials.ts @@ -0,0 +1,55 @@ +import * as oauth from "oauth4webapi"; +import { SPOTIFY_AUTH_URL } from "@soundify/web-api"; +import { z } from "zod"; +import { load } from "std/dotenv/mod.ts"; +import { search, SpotifyClient } from "@soundify/web-api"; + +await load({ export: true }); + +const env = z + .object({ + SPOTIFY_CLIENT_ID: z.string(), + SPOTIFY_CLIENT_SECRET: z.string(), + }) + .parse(Deno.env.toObject()); + +const issuer = new URL(SPOTIFY_AUTH_URL); +const authServer = await oauth.processDiscoveryResponse( + issuer, + await oauth.discoveryRequest(issuer), +); + +const oauthClient: oauth.Client = { + client_id: env.SPOTIFY_CLIENT_ID, + client_secret: env.SPOTIFY_CLIENT_SECRET, + token_endpoint_auth_method: "client_secret_basic", +}; + +const refresher = async () => { + const res = await oauth.clientCredentialsGrantRequest( + authServer, + oauthClient, + {}, + ); + + const result = await oauth.processClientCredentialsResponse( + authServer, + oauthClient, + res, + ); + if (oauth.isOAuth2Error(result)) { + throw new Error( + result.error + result.error_description + ? " : " + result.error_description + : "", + ); + } + return result.access_token; +}; + +const accessToken = await refresher(); + +const spotifyClient = new SpotifyClient(accessToken); + +const result = await search(spotifyClient, "track", "Never Gonna Give You Up"); +console.log(result.tracks.items.at(0)); diff --git a/examples/oauth/pkce_code.ts b/examples/oauth/pkce_code.ts new file mode 100644 index 0000000..1d117ef --- /dev/null +++ b/examples/oauth/pkce_code.ts @@ -0,0 +1,121 @@ +import * as oauth from "oauth4webapi"; +import { + getCurrentUser, + OAUTH_SCOPES, + SPOTIFY_AUTH_URL, + SpotifyClient, +} from "@soundify/web-api"; +import { z } from "zod"; +import { load } from "std/dotenv/mod.ts"; +import { Application, Router } from "oak"; + +await load({ export: true }); + +const env = z + .object({ + SPOTIFY_CLIENT_ID: z.string(), + SPOTIFY_REDIRECT_URI: z.string().url(), + }) + .parse(Deno.env.toObject()); + +const issuer = new URL(SPOTIFY_AUTH_URL); +const authServer = await oauth.processDiscoveryResponse( + issuer, + await oauth.discoveryRequest(issuer), +); + +const oauthClient: oauth.Client = { + client_id: env.SPOTIFY_CLIENT_ID, + token_endpoint_auth_method: "client_secret_basic", +}; + +const app = new Application(); +const router = new Router(); + +router.get("/login", async (ctx) => { + const codeVerifier = oauth.generateRandomCodeVerifier(); + const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier); + + await ctx.cookies.set("code_verifier", codeVerifier, { + httpOnly: true, + path: "/callback", + }); + + const authUrl = new URL(authServer.authorization_endpoint!); + authUrl.searchParams.set("client_id", env.SPOTIFY_CLIENT_ID); + authUrl.searchParams.set("redirect_uri", env.SPOTIFY_REDIRECT_URI); + authUrl.searchParams.set("response_type", "code"); + authUrl.searchParams.set("code_challenge", codeChallenge); + authUrl.searchParams.set("code_challenge_method", "S256"); + authUrl.searchParams.set( + "scope", + Object.values(OAUTH_SCOPES).join(" "), + ); + + return ctx.response.redirect(authUrl); +}); + +router.get("/callback", async (ctx) => { + try { + const params = oauth.validateAuthResponse( + authServer, + oauthClient, + ctx.request.url, + oauth.expectNoState, + ); + if (oauth.isOAuth2Error(params)) { + throw new Error( + params.error + params.error_description + ? " : " + params.error_description + : "", + ); + } + + const codeVerifier = await ctx.cookies.get("code_verifier"); + if (!codeVerifier) { + throw new Error("no code verifier"); + } + + const response = await oauth.authorizationCodeGrantRequest( + authServer, + oauthClient, + params, + env.SPOTIFY_REDIRECT_URI, + codeVerifier, + ); + const result = await oauth.processAuthorizationCodeOAuth2Response( + authServer, + oauthClient, + response, + ); + console.log(result); + if (oauth.isOAuth2Error(result)) { + throw new Error(result.error); + } + + const spotifyClient = new SpotifyClient(result.access_token); + const user = await getCurrentUser(spotifyClient); + + ctx.response.type = "application/json"; + ctx.response.body = JSON.stringify(user); + ctx.response.status = 200; + } catch (error) { + console.log(error); + ctx.response.status = 500; + ctx.response.body = error.message; + } finally { + await ctx.cookies.set("code_verifier", null, { + httpOnly: true, + path: "/callback", + }); + } +}); + +app.use(router.routes()); +app.use(router.allowedMethods()); + +app.addEventListener( + "listen", + (event) => console.log(`http://${event.hostname}:${event.port}/login`), +); +await app.listen({ port: 3000 }); diff --git a/examples/react-implicit-grant/.env.example b/examples/react-implicit-grant/.env.example deleted file mode 100644 index 34829e3..0000000 --- a/examples/react-implicit-grant/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -VITE_SPOTIFY_CLIENT_ID= -VITE_SPOTIFY_REDIRECT_URI= \ No newline at end of file diff --git a/examples/react-implicit-grant/README.md b/examples/react-implicit-grant/README.md deleted file mode 100644 index f86bd69..0000000 --- a/examples/react-implicit-grant/README.md +++ /dev/null @@ -1,5 +0,0 @@ -Soundify example banner - -# Soundify exapmle (react-implicit-grant) - -// TODO diff --git a/examples/react-implicit-grant/index.html b/examples/react-implicit-grant/index.html deleted file mode 100644 index 1df520a..0000000 --- a/examples/react-implicit-grant/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - Soundify example (react-implicit-grant) - - - -
- - - diff --git a/examples/react-implicit-grant/package.json b/examples/react-implicit-grant/package.json deleted file mode 100644 index 20d9526..0000000 --- a/examples/react-implicit-grant/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@examples/react-implicit-grant", - "private": "true", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build" - }, - "devDependencies": { - "@types/react": "latest", - "@types/react-dom": "latest", - "@vitejs/plugin-react": "3.1.0", - "typescript": "5.1.3", - "vite": "4.3.9" - }, - "dependencies": { - "@tanstack/react-query": "4.29.14", - "react": "18.2.0", - "react-dom": "18.2.0", - "react-router-dom": "6.13.0", - "@soundify/web-api": "workspace:*" - } -} diff --git a/examples/react-implicit-grant/src/env.d.ts b/examples/react-implicit-grant/src/env.d.ts deleted file mode 100644 index 1792def..0000000 --- a/examples/react-implicit-grant/src/env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// - -interface ImportMetaEnv { - readonly VITE_SPOTIFY_CLIENT_ID: string; - readonly VITE_SPOTIFY_REDIRECT_URI: string; -} diff --git a/examples/react-implicit-grant/src/main.tsx b/examples/react-implicit-grant/src/main.tsx deleted file mode 100644 index a625373..0000000 --- a/examples/react-implicit-grant/src/main.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; -import { createRoot } from "react-dom/client"; -import { Page as IndexPage } from "./pages/index"; -import { Page as CallbackPage } from "./pages/callback"; -import { BrowserRouter, Route, Routes } from "react-router-dom"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { SpotifyProvider } from "./spotify"; - -const queryClient = new QueryClient(); - -createRoot(document.getElementById("root")!).render( - - - - - - } /> - } /> - - - - - -); diff --git a/examples/react-implicit-grant/src/pages/callback.tsx b/examples/react-implicit-grant/src/pages/callback.tsx deleted file mode 100644 index 3210d21..0000000 --- a/examples/react-implicit-grant/src/pages/callback.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useHandleCallback } from "../spotify"; - -export const Page = () => { - const { status, error } = useHandleCallback(); - - if (status === "loading") { - return

Loading...

; - } - if (status === "error") { - return

{String(error)}

; - } - - location.replace("/"); - return

Successfully authorized

; -}; diff --git a/examples/react-implicit-grant/src/pages/index.tsx b/examples/react-implicit-grant/src/pages/index.tsx deleted file mode 100644 index db0b739..0000000 --- a/examples/react-implicit-grant/src/pages/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getCurrentUser } from "@soundify/web-api"; -import { useSpotifyClinet } from "../spotify"; - -export const Page = () => { - const client = useSpotifyClinet(); - - const { - status, - data: userProfile, - error - } = useQuery({ - queryKey: ["user-profile"], - queryFn: () => getCurrentUser(client), - retry: false - }); - - if (status === "error") { - return

{String(error)}

; - } - - if (status === "loading") { - return

Loading...

; - } - - return

Welcome {userProfile.display_name}!

; -}; diff --git a/examples/react-implicit-grant/src/spotify.tsx b/examples/react-implicit-grant/src/spotify.tsx deleted file mode 100644 index bf34afb..0000000 --- a/examples/react-implicit-grant/src/spotify.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { ImplicitFlow, SpotifyClient } from "@soundify/web-api"; -import { createContext, ReactNode, useContext } from "react"; -import { useQuery } from "@tanstack/react-query"; - -export const SpotifyContext = createContext(null); - -const env = { - client_id: import.meta.env.VITE_SPOTIFY_CLIENT_ID, - redirect_uri: import.meta.env.VITE_SPOTIFY_REDIRECT_URI -}; - -const authFlow = new ImplicitFlow(env.client_id); - -const authorize = () => { - const state = crypto.randomUUID(); - localStorage.setItem("state", state); - - location.replace( - authFlow.getAuthURL({ - scopes: ["user-read-email"], - state, - redirect_uri: env.redirect_uri - }) - ); -}; - -export const SpotifyProvider = ({ children }: { children: ReactNode }) => { - if (location.pathname === "/callback") { - return <>{children}; - } - - const accessToken = localStorage.getItem("SPOTIFY_ACCESS_TOKEN"); - if (!accessToken) { - authorize(); - return

Redirecting...

; - } - - const client = new SpotifyClient(accessToken, { onUnauthorized: authorize }); - - return ( - {children} - ); -}; - -export const useSpotifyClinet = () => { - const client = useContext(SpotifyContext); - if (!client) { - throw new Error("Unreachable"); - } - return client; -}; - -export const useHandleCallback = () => { - return useQuery({ - staleTime: Infinity, - retry: false, - queryFn: () => { - const params = ImplicitFlow.parseCallbackData(location.hash); - if ("error" in params) { - throw new Error(params.error); - } - - const storedState = localStorage.getItem("state"); - if (!storedState || !params.state || storedState !== params.state) { - throw new Error("Invalid state"); - } - - localStorage.removeItem("state"); - localStorage.setItem("SPOTIFY_ACCESS_TOKEN", params.access_token); - return true; - } - }); -}; diff --git a/examples/react-implicit-grant/tsconfig.json b/examples/react-implicit-grant/tsconfig.json deleted file mode 100644 index 87c2fde..0000000 --- a/examples/react-implicit-grant/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "forceConsistentCasingInFileNames": true, - "allowSyntheticDefaultImports": true, - "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, - "noUnusedParameters": true, - "strict": true, - "declaration": true, - "moduleResolution": "node", - "module": "ESNext", - "rootDir": ".", - "outDir": "./dist/", - "target": "ESNext", - "jsx": "preserve" - } -} diff --git a/examples/react-implicit-grant/vite.config.ts b/examples/react-implicit-grant/vite.config.ts deleted file mode 100644 index dbf7d60..0000000 --- a/examples/react-implicit-grant/vite.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], - server: { - port: 3000 - } -}); diff --git a/examples/react-pkce-auth/.env.example b/examples/react-pkce-auth/.env.example deleted file mode 100644 index c64bea8..0000000 --- a/examples/react-pkce-auth/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -VITE_SPOTIFY_CLIENT_ID= -VITE_SPOTIFY_REDIRECT_URI=http://localhost:3000/callback \ No newline at end of file diff --git a/examples/react-pkce-auth/README.md b/examples/react-pkce-auth/README.md deleted file mode 100644 index 288cf26..0000000 --- a/examples/react-pkce-auth/README.md +++ /dev/null @@ -1,14 +0,0 @@ -Soundify example banner - -# Soundify exapmle (react-pkce-auth) - -This example shows how to use PKCE authentication in a single-page React -application. We will also use the `react-router-dom`, `@tanstack/react-query` -libraries to speed up our routine workflow. - -Don't forget to fill in the env variables in the `.env` file in the same way as -in the `.env.example` file. - -After logging to your spotify you will see your top artists - - diff --git a/examples/react-pkce-auth/index.html b/examples/react-pkce-auth/index.html deleted file mode 100644 index f3c41a0..0000000 --- a/examples/react-pkce-auth/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Soundify example (pkce-auth) - - -
- - - diff --git a/examples/react-pkce-auth/package.json b/examples/react-pkce-auth/package.json deleted file mode 100644 index d4eb043..0000000 --- a/examples/react-pkce-auth/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@examples/react-pkce-auth", - "private": "true", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build" - }, - "devDependencies": { - "@types/react": "latest", - "@types/react-dom": "latest", - "@vitejs/plugin-react": "3.1.0", - "typescript": "5.1.3", - "vite": "4.3.9" - }, - "dependencies": { - "@tanstack/react-query": "4.29.14", - "react": "18.2.0", - "react-dom": "18.2.0", - "react-router-dom": "6.13.0", - "@soundify/web-api": "workspace:*" - } -} diff --git a/examples/react-pkce-auth/src/env.d.ts b/examples/react-pkce-auth/src/env.d.ts deleted file mode 100644 index 1792def..0000000 --- a/examples/react-pkce-auth/src/env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// - -interface ImportMetaEnv { - readonly VITE_SPOTIFY_CLIENT_ID: string; - readonly VITE_SPOTIFY_REDIRECT_URI: string; -} diff --git a/examples/react-pkce-auth/src/main.tsx b/examples/react-pkce-auth/src/main.tsx deleted file mode 100644 index d359bdf..0000000 --- a/examples/react-pkce-auth/src/main.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; -import { createRoot } from "react-dom/client"; -import { Page as IndexPage } from "./pages/index"; -import { Page as CallbackPage } from "./pages/callback"; -import { BrowserRouter, Route, Routes } from "react-router-dom"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { SpotifyProvider } from "./spotify"; - -const queryClient = new QueryClient(); - -createRoot(document.getElementById("root")!).render( - - - - - - } /> - } /> - - - - - -); diff --git a/examples/react-pkce-auth/src/pages/callback.tsx b/examples/react-pkce-auth/src/pages/callback.tsx deleted file mode 100644 index 3210d21..0000000 --- a/examples/react-pkce-auth/src/pages/callback.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useHandleCallback } from "../spotify"; - -export const Page = () => { - const { status, error } = useHandleCallback(); - - if (status === "loading") { - return

Loading...

; - } - if (status === "error") { - return

{String(error)}

; - } - - location.replace("/"); - return

Successfully authorized

; -}; diff --git a/examples/react-pkce-auth/src/pages/index.tsx b/examples/react-pkce-auth/src/pages/index.tsx deleted file mode 100644 index 3f45143..0000000 --- a/examples/react-pkce-auth/src/pages/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { getUserTopItems } from "@soundify/web-api"; -import { useSpotifyClient } from "../spotify"; - -export const Page = () => { - const client = useSpotifyClient(); - - const { - status, - data: topArtists, - error - } = useQuery({ - queryKey: ["user-profile"], - queryFn: () => getUserTopItems(client, "artists"), - retry: false - }); - - if (status === "error") { - return

{String(error)}

; - } - if (status === "loading") { - return

Loading...

; - } - - return ( -
-

Your top artists

-
    - {topArtists.items.map((artist) => ( -
  • - -

    {artist.name}

    -

    Popularity: {artist.popularity}

    -
  • - ))} -
-
- ); -}; diff --git a/examples/react-pkce-auth/src/spotify.tsx b/examples/react-pkce-auth/src/spotify.tsx deleted file mode 100644 index 93629bb..0000000 --- a/examples/react-pkce-auth/src/spotify.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { createContext, ReactNode, useContext, useMemo } from "react"; -import { PKCECodeFlow, SpotifyClient } from "@soundify/web-api"; - -const appName = "react-pkce-auth"; - -export const SPOTIFY_REFRESH_TOKEN = appName + "-refresh-token"; -export const SPOTIFY_ACCESS_TOKNE = appName + "-access-token"; -export const CODE_VERIFIER = appName + "-code-verifier"; - -const SpotifyContext = createContext(null); - -const env = { - client_id: import.meta.env.VITE_SPOTIFY_CLIENT_ID, - redirect_uri: import.meta.env.VITE_SPOTIFY_REDIRECT_URI -}; - -const authFlow = new PKCECodeFlow(env.client_id); - -const authorize = async () => { - const { code_challenge, code_verifier } = await PKCECodeFlow.generateCodes(); - localStorage.setItem(CODE_VERIFIER, code_verifier); - - location.replace( - authFlow.getAuthURL({ - code_challenge, - scopes: ["user-read-private", "user-top-read"], - redirect_uri: env.redirect_uri - }) - ); -}; - -export const SpotifyProvider = ({ children }: { children: ReactNode }) => { - const client = useMemo(() => { - const access_token = localStorage.getItem(SPOTIFY_ACCESS_TOKNE); - const refresh_token = localStorage.getItem(SPOTIFY_REFRESH_TOKEN); - if (!refresh_token) return null; - - const refresher = authFlow.createRefresher(refresh_token); - - return new SpotifyClient( - { - refresh: async () => { - const { access_token, refresh_token } = await refresher(); - - localStorage.setItem(SPOTIFY_ACCESS_TOKNE, access_token); - localStorage.setItem(SPOTIFY_REFRESH_TOKEN, refresh_token); - - return access_token; - }, - token: access_token ?? undefined - }, - { onUnauthorized: authorize } - ); - }, []); - - if (location.pathname === "/callback") { - return <>{children}; - } - - if (client === null) { - authorize(); - return

Redirecting...

; - } - - return ( - {children} - ); -}; - -export const useSpotifyClient = () => { - const spotifyContext = useContext(SpotifyContext); - if (spotifyContext === null) { - throw new Error("Unreachable: SpotifyContext is null"); - } - - return spotifyContext; -}; - -export const useHandleCallback = () => { - return useQuery({ - queryKey: ["spotify-callback"], - queryFn: async () => { - const data = PKCECodeFlow.parseCallbackData( - new URLSearchParams(location.search) - ); - if ("error" in data) { - throw new Error(data.error); - } - - const code_verifier = localStorage.getItem(CODE_VERIFIER); - if (!code_verifier) { - throw new Error("Cannot find code_verifier"); - } - - return await authFlow.getGrantData({ - code: data.code, - code_verifier, - redirect_uri: env.redirect_uri - }); - }, - staleTime: Infinity, - onSuccess: ({ access_token, refresh_token }) => { - localStorage.removeItem(CODE_VERIFIER); - localStorage.setItem(SPOTIFY_REFRESH_TOKEN, refresh_token); - localStorage.setItem(SPOTIFY_ACCESS_TOKNE, access_token); - }, - retry: false - }); -}; diff --git a/examples/react-pkce-auth/tsconfig.json b/examples/react-pkce-auth/tsconfig.json deleted file mode 100644 index 87c2fde..0000000 --- a/examples/react-pkce-auth/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "forceConsistentCasingInFileNames": true, - "allowSyntheticDefaultImports": true, - "noFallthroughCasesInSwitch": true, - "noImplicitReturns": true, - "noUnusedParameters": true, - "strict": true, - "declaration": true, - "moduleResolution": "node", - "module": "ESNext", - "rootDir": ".", - "outDir": "./dist/", - "target": "ESNext", - "jsx": "preserve" - } -} diff --git a/examples/react-pkce-auth/vite.config.ts b/examples/react-pkce-auth/vite.config.ts deleted file mode 100644 index dbf7d60..0000000 --- a/examples/react-pkce-auth/vite.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], - server: { - port: 3000 - } -}); diff --git a/package.json b/package.json index 2771d99..b0f8cda 100644 --- a/package.json +++ b/package.json @@ -1,82 +1,55 @@ { "name": "@soundify/web-api", - "version": "0.2.4", - "description": "Modern Spotify api wrapper for Node, Deno, and browser 🎧", + "version": "1.0.0-rc1", + "description": "🎧 Spotify Web API client for js/ts runtime environments", "type": "module", - "main": "dist/server.cjs", - "module": "dist/server.js", - "types": "types/index.d.ts", - "browser": { - "dist/server.cjs": "dist/browser.cjs", - "dist/server.js": "dist/browser.js" - }, + "main": "dist/mod.cjs", + "module": "dist/mod.js", + "types": "dist/mod.d.ts", "license": "MIT", + "sideEffects": false, "files": [ "dist/", - "types/", - "src/", "LICENSE", "package.json", "README.md" ], "exports": { ".": { - "types": "./types/index.d.ts", - "browser": { - "import": "./dist/browser.js", - "require": "./dist/browser.cjs" - }, - "node": { - "import": "./dist/server.js", - "require": "./dist/server.cjs" - }, - "deno": "./dist/server.js", - "bun": "./dist/server.js", - "import": "./dist/server.js", - "require": "./dist/server.cjs" + "import": "./dist/mod.js", + "require": "./dist/mod.cjs" + }, + "./pagination": { + "import": "./dist/pagination.js", + "require": "./dist/pagination.cjs" } }, "scripts": { - "build": "rimraf dist/ types/ && __IS_NODE__=true vite build && __IS_NODE__=false vite build && tsc --emitDeclarationOnly --declaration", - "types:check": "tsc --noEmit", - "test": "vitest run", - "test:coverage": "vitest run --coverage", - "lint:check": "prettier --check ./src/ ./examples", - "lint": "prettier --write ./src ./examples" + "build": "tsup ./src/mod.ts ./src/pagination.ts --format esm,cjs --minify --dts --out-dir dist && rm ./dist/*.d.cts" }, "devDependencies": { - "@types/node": "20.3.1", - "all-contributors-cli": "6.26.0", - "envalid": "7.3.1", - "prettier": "2.8.8", - "rimraf": "5.0.1", - "typescript": "5.1.3", - "vite": "4.3.9", - "vitest": "0.32.2", - "@vitest/coverage-c8": "0.32.2", - "vitest-fetch-mock": "0.2.2", - "@faker-js/faker": "8.0.2" + "all-contributors-cli": "6.26.1", + "typescript": "5.3.3", + "tsup": "8.0.1" }, "author": { "name": "Artem Melnyk", "url": "https://github.com/MellKam" }, - "packageManager": "pnpm@8.6.3", "repository": "github:MellKam/soundify", "bugs": { "url": "https://github.com/MellKam/soundify/issues" }, "readme": "https://github.com/MellKam/soundify#readme", + "publishConfig": { + "access": "public" + }, "keywords": [ "spotify", + "web", "api", - "wrapper", "music", - "client", - "soundify", - "web", - "js", - "ts", - "deno" + "sdk", + "soundify" ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0b3c4d..e66e587 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,427 +4,37 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -importers: - - .: - devDependencies: - '@faker-js/faker': - specifier: 8.0.2 - version: 8.0.2 - '@types/node': - specifier: 20.3.1 - version: 20.3.1 - '@vitest/coverage-c8': - specifier: 0.32.2 - version: 0.32.2(vitest@0.32.2) - all-contributors-cli: - specifier: 6.26.0 - version: 6.26.0 - envalid: - specifier: 7.3.1 - version: 7.3.1 - prettier: - specifier: 2.8.8 - version: 2.8.8 - rimraf: - specifier: 5.0.1 - version: 5.0.1 - typescript: - specifier: 5.1.3 - version: 5.1.3 - vite: - specifier: 4.3.9 - version: 4.3.9(@types/node@20.3.1) - vitest: - specifier: 0.32.2 - version: 0.32.2 - vitest-fetch-mock: - specifier: 0.2.2 - version: 0.2.2(vitest@0.32.2) - - examples/next-ssr: - dependencies: - '@soundify/web-api': - specifier: workspace:* - version: link:../.. - '@tanstack/react-query': - specifier: 4.29.14 - version: 4.29.14(react-dom@18.2.0)(react@18.2.0) - '@types/node': - specifier: latest - version: 20.3.1 - '@types/react': - specifier: latest - version: 18.2.13 - '@types/react-dom': - specifier: latest - version: 18.0.11 - cookies-next: - specifier: 2.1.2 - version: 2.1.2 - next: - specifier: 13.4.6 - version: 13.4.6(react-dom@18.2.0)(react@18.2.0) - react: - specifier: 18.2.0 - version: 18.2.0 - react-dom: - specifier: 18.2.0 - version: 18.2.0(react@18.2.0) - typescript: - specifier: 5.1.3 - version: 5.1.3 - - examples/node-express-auth: - dependencies: - '@soundify/web-api': - specifier: workspace:* - version: link:../.. - cookie-parser: - specifier: 1.4.6 - version: 1.4.6 - express: - specifier: 4.18.2 - version: 4.18.2 - devDependencies: - '@types/cookie-parser': - specifier: 1.4.3 - version: 1.4.3 - '@types/express': - specifier: 4.17.17 - version: 4.17.17 - '@types/node': - specifier: latest - version: 20.3.1 - typescript: - specifier: 5.1.3 - version: 5.1.3 - - examples/react-implicit-grant: - dependencies: - '@soundify/web-api': - specifier: workspace:* - version: link:../.. - '@tanstack/react-query': - specifier: 4.29.14 - version: 4.29.14(react-dom@18.2.0)(react@18.2.0) - react: - specifier: 18.2.0 - version: 18.2.0 - react-dom: - specifier: 18.2.0 - version: 18.2.0(react@18.2.0) - react-router-dom: - specifier: 6.13.0 - version: 6.13.0(react-dom@18.2.0)(react@18.2.0) - devDependencies: - '@types/react': - specifier: latest - version: 18.2.13 - '@types/react-dom': - specifier: latest - version: 18.0.11 - '@vitejs/plugin-react': - specifier: 3.1.0 - version: 3.1.0(vite@4.3.9) - typescript: - specifier: 5.1.3 - version: 5.1.3 - vite: - specifier: 4.3.9 - version: 4.3.9(@types/node@20.3.1) - - examples/react-pkce-auth: - dependencies: - '@soundify/web-api': - specifier: workspace:* - version: link:../.. - '@tanstack/react-query': - specifier: 4.29.14 - version: 4.29.14(react-dom@18.2.0)(react@18.2.0) - react: - specifier: 18.2.0 - version: 18.2.0 - react-dom: - specifier: 18.2.0 - version: 18.2.0(react@18.2.0) - react-router-dom: - specifier: 6.13.0 - version: 6.13.0(react-dom@18.2.0)(react@18.2.0) - devDependencies: - '@types/react': - specifier: latest - version: 18.2.13 - '@types/react-dom': - specifier: latest - version: 18.0.11 - '@vitejs/plugin-react': - specifier: 3.1.0 - version: 3.1.0(vite@4.3.9) - typescript: - specifier: 5.1.3 - version: 5.1.3 - vite: - specifier: 4.3.9 - version: 4.3.9(@types/node@20.3.1) +devDependencies: + all-contributors-cli: + specifier: 6.26.1 + version: 6.26.1 + tsup: + specifier: 8.0.1 + version: 8.0.1(typescript@5.3.3) + typescript: + specifier: 5.3.3 + version: 5.3.3 packages: - /@ampproject/remapping@2.2.1: - resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 - dev: true - - /@babel/code-frame@7.21.4: - resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.18.6 - dev: true - - /@babel/compat-data@7.21.7: - resolution: {integrity: sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/core@7.21.5: - resolution: {integrity: sha512-9M398B/QH5DlfCOTKDZT1ozXr0x8uBEeFd+dJraGUZGiaNpGCDVGCc14hZexsMblw3XxltJ+6kSvogp9J+5a9g==} - engines: {node: '>=6.9.0'} - dependencies: - '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.21.4 - '@babel/generator': 7.21.5 - '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.5) - '@babel/helper-module-transforms': 7.21.5 - '@babel/helpers': 7.21.5 - '@babel/parser': 7.21.5 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.5 - '@babel/types': 7.21.5 - convert-source-map: 1.9.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/generator@7.21.5: - resolution: {integrity: sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 - jsesc: 2.5.2 - dev: true - - /@babel/helper-compilation-targets@7.21.5(@babel/core@7.21.5): - resolution: {integrity: sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.21.7 - '@babel/core': 7.21.5 - '@babel/helper-validator-option': 7.21.0 - browserslist: 4.21.5 - lru-cache: 5.1.1 - semver: 6.3.0 - dev: true - - /@babel/helper-environment-visitor@7.21.5: - resolution: {integrity: sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-function-name@7.21.0: - resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.20.7 - '@babel/types': 7.21.5 - dev: true - - /@babel/helper-hoist-variables@7.18.6: - resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - dev: true - - /@babel/helper-module-imports@7.21.4: - resolution: {integrity: sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.4 - dev: true - - /@babel/helper-module-transforms@7.21.5: - resolution: {integrity: sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.21.5 - '@babel/helper-module-imports': 7.21.4 - '@babel/helper-simple-access': 7.21.5 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/helper-validator-identifier': 7.19.1 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.5 - '@babel/types': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/helper-plugin-utils@7.20.2: - resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-simple-access@7.21.5: - resolution: {integrity: sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - dev: true - - /@babel/helper-split-export-declaration@7.18.6: - resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - dev: true - - /@babel/helper-string-parser@7.19.4: - resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-string-parser@7.21.5: - resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-validator-identifier@7.19.1: - resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-validator-option@7.21.0: - resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helpers@7.21.5: - resolution: {integrity: sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.5 - '@babel/types': 7.21.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/highlight@7.18.6: - resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.19.1 - chalk: 2.4.2 - js-tokens: 4.0.0 - dev: true - - /@babel/parser@7.21.5: - resolution: {integrity: sha512-J+IxH2IsxV4HbnTrSWgMAQj0UEo61hDA4Ny8h8PCX0MLXiibqHbqIOVneqdocemSBc22VpBKxt4J6FQzy9HarQ==} - engines: {node: '>=6.0.0'} - hasBin: true - dependencies: - '@babel/types': 7.21.5 - dev: true - - /@babel/plugin-transform-react-jsx-self@7.21.0(@babel/core@7.21.5): - resolution: {integrity: sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==} + /@babel/runtime@7.23.9: + resolution: {integrity: sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.5 - '@babel/helper-plugin-utils': 7.20.2 + regenerator-runtime: 0.14.1 dev: true - /@babel/plugin-transform-react-jsx-source@7.19.6(@babel/core@7.21.5): - resolution: {integrity: sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.21.5 - '@babel/helper-plugin-utils': 7.20.2 - dev: true - - /@babel/runtime@7.21.5: - resolution: {integrity: sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.13.11 - dev: true - - /@babel/template@7.20.7: - resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.21.4 - '@babel/parser': 7.21.5 - '@babel/types': 7.21.5 - dev: true - - /@babel/traverse@7.21.5: - resolution: {integrity: sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.21.4 - '@babel/generator': 7.21.5 - '@babel/helper-environment-visitor': 7.21.5 - '@babel/helper-function-name': 7.21.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.21.5 - '@babel/types': 7.21.5 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: true - - /@babel/types@7.21.4: - resolution: {integrity: sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.19.4 - '@babel/helper-validator-identifier': 7.19.1 - to-fast-properties: 2.0.0 - dev: true - - /@babel/types@7.21.5: - resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.21.5 - '@babel/helper-validator-identifier': 7.19.1 - to-fast-properties: 2.0.0 - dev: true - - /@bcoe/v8-coverage@0.2.3: - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + /@esbuild/aix-ppc64@0.19.12: + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true dev: true + optional: true - /@esbuild/android-arm64@0.17.15: - resolution: {integrity: sha512-0kOB6Y7Br3KDVgHeg8PRcvfLkq+AccreK///B4Z6fNZGr/tNHX0z2VywCc7PTeWp+bPvjA5WMvNXltHw5QjAIA==} + /@esbuild/android-arm64@0.19.12: + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} engines: {node: '>=12'} cpu: [arm64] os: [android] @@ -432,8 +42,8 @@ packages: dev: true optional: true - /@esbuild/android-arm@0.17.15: - resolution: {integrity: sha512-sRSOVlLawAktpMvDyJIkdLI/c/kdRTOqo8t6ImVxg8yT7LQDUYV5Rp2FKeEosLr6ZCja9UjYAzyRSxGteSJPYg==} + /@esbuild/android-arm@0.19.12: + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} engines: {node: '>=12'} cpu: [arm] os: [android] @@ -441,8 +51,8 @@ packages: dev: true optional: true - /@esbuild/android-x64@0.17.15: - resolution: {integrity: sha512-MzDqnNajQZ63YkaUWVl9uuhcWyEyh69HGpMIrf+acR4otMkfLJ4sUCxqwbCyPGicE9dVlrysI3lMcDBjGiBBcQ==} + /@esbuild/android-x64@0.19.12: + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} engines: {node: '>=12'} cpu: [x64] os: [android] @@ -450,8 +60,8 @@ packages: dev: true optional: true - /@esbuild/darwin-arm64@0.17.15: - resolution: {integrity: sha512-7siLjBc88Z4+6qkMDxPT2juf2e8SJxmsbNVKFY2ifWCDT72v5YJz9arlvBw5oB4W/e61H1+HDB/jnu8nNg0rLA==} + /@esbuild/darwin-arm64@0.19.12: + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] @@ -459,8 +69,8 @@ packages: dev: true optional: true - /@esbuild/darwin-x64@0.17.15: - resolution: {integrity: sha512-NbImBas2rXwYI52BOKTW342Tm3LTeVlaOQ4QPZ7XuWNKiO226DisFk/RyPk3T0CKZkKMuU69yOvlapJEmax7cg==} + /@esbuild/darwin-x64@0.19.12: + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} engines: {node: '>=12'} cpu: [x64] os: [darwin] @@ -468,8 +78,8 @@ packages: dev: true optional: true - /@esbuild/freebsd-arm64@0.17.15: - resolution: {integrity: sha512-Xk9xMDjBVG6CfgoqlVczHAdJnCs0/oeFOspFap5NkYAmRCT2qTn1vJWA2f419iMtsHSLm+O8B6SLV/HlY5cYKg==} + /@esbuild/freebsd-arm64@0.19.12: + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] @@ -477,8 +87,8 @@ packages: dev: true optional: true - /@esbuild/freebsd-x64@0.17.15: - resolution: {integrity: sha512-3TWAnnEOdclvb2pnfsTWtdwthPfOz7qAfcwDLcfZyGJwm1SRZIMOeB5FODVhnM93mFSPsHB9b/PmxNNbSnd0RQ==} + /@esbuild/freebsd-x64@0.19.12: + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] @@ -486,8 +96,8 @@ packages: dev: true optional: true - /@esbuild/linux-arm64@0.17.15: - resolution: {integrity: sha512-T0MVnYw9KT6b83/SqyznTs/3Jg2ODWrZfNccg11XjDehIved2oQfrX/wVuev9N936BpMRaTR9I1J0tdGgUgpJA==} + /@esbuild/linux-arm64@0.19.12: + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} engines: {node: '>=12'} cpu: [arm64] os: [linux] @@ -495,8 +105,8 @@ packages: dev: true optional: true - /@esbuild/linux-arm@0.17.15: - resolution: {integrity: sha512-MLTgiXWEMAMr8nmS9Gigx43zPRmEfeBfGCwxFQEMgJ5MC53QKajaclW6XDPjwJvhbebv+RzK05TQjvH3/aM4Xw==} + /@esbuild/linux-arm@0.19.12: + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} engines: {node: '>=12'} cpu: [arm] os: [linux] @@ -504,8 +114,8 @@ packages: dev: true optional: true - /@esbuild/linux-ia32@0.17.15: - resolution: {integrity: sha512-wp02sHs015T23zsQtU4Cj57WiteiuASHlD7rXjKUyAGYzlOKDAjqK6bk5dMi2QEl/KVOcsjwL36kD+WW7vJt8Q==} + /@esbuild/linux-ia32@0.19.12: + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} engines: {node: '>=12'} cpu: [ia32] os: [linux] @@ -513,8 +123,8 @@ packages: dev: true optional: true - /@esbuild/linux-loong64@0.17.15: - resolution: {integrity: sha512-k7FsUJjGGSxwnBmMh8d7IbObWu+sF/qbwc+xKZkBe/lTAF16RqxRCnNHA7QTd3oS2AfGBAnHlXL67shV5bBThQ==} + /@esbuild/linux-loong64@0.19.12: + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} engines: {node: '>=12'} cpu: [loong64] os: [linux] @@ -522,8 +132,8 @@ packages: dev: true optional: true - /@esbuild/linux-mips64el@0.17.15: - resolution: {integrity: sha512-ZLWk6czDdog+Q9kE/Jfbilu24vEe/iW/Sj2d8EVsmiixQ1rM2RKH2n36qfxK4e8tVcaXkvuV3mU5zTZviE+NVQ==} + /@esbuild/linux-mips64el@0.19.12: + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] @@ -531,8 +141,8 @@ packages: dev: true optional: true - /@esbuild/linux-ppc64@0.17.15: - resolution: {integrity: sha512-mY6dPkIRAiFHRsGfOYZC8Q9rmr8vOBZBme0/j15zFUKM99d4ILY4WpOC7i/LqoY+RE7KaMaSfvY8CqjJtuO4xg==} + /@esbuild/linux-ppc64@0.19.12: + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] @@ -540,8 +150,8 @@ packages: dev: true optional: true - /@esbuild/linux-riscv64@0.17.15: - resolution: {integrity: sha512-EcyUtxffdDtWjjwIH8sKzpDRLcVtqANooMNASO59y+xmqqRYBBM7xVLQhqF7nksIbm2yHABptoioS9RAbVMWVA==} + /@esbuild/linux-riscv64@0.19.12: + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] @@ -549,8 +159,8 @@ packages: dev: true optional: true - /@esbuild/linux-s390x@0.17.15: - resolution: {integrity: sha512-BuS6Jx/ezxFuHxgsfvz7T4g4YlVrmCmg7UAwboeyNNg0OzNzKsIZXpr3Sb/ZREDXWgt48RO4UQRDBxJN3B9Rbg==} + /@esbuild/linux-s390x@0.19.12: + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} engines: {node: '>=12'} cpu: [s390x] os: [linux] @@ -558,8 +168,8 @@ packages: dev: true optional: true - /@esbuild/linux-x64@0.17.15: - resolution: {integrity: sha512-JsdS0EgEViwuKsw5tiJQo9UdQdUJYuB+Mf6HxtJSPN35vez1hlrNb1KajvKWF5Sa35j17+rW1ECEO9iNrIXbNg==} + /@esbuild/linux-x64@0.19.12: + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} engines: {node: '>=12'} cpu: [x64] os: [linux] @@ -567,8 +177,8 @@ packages: dev: true optional: true - /@esbuild/netbsd-x64@0.17.15: - resolution: {integrity: sha512-R6fKjtUysYGym6uXf6qyNephVUQAGtf3n2RCsOST/neIwPqRWcnc3ogcielOd6pT+J0RDR1RGcy0ZY7d3uHVLA==} + /@esbuild/netbsd-x64@0.19.12: + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] @@ -576,8 +186,8 @@ packages: dev: true optional: true - /@esbuild/openbsd-x64@0.17.15: - resolution: {integrity: sha512-mVD4PGc26b8PI60QaPUltYKeSX0wxuy0AltC+WCTFwvKCq2+OgLP4+fFd+hZXzO2xW1HPKcytZBdjqL6FQFa7w==} + /@esbuild/openbsd-x64@0.19.12: + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] @@ -585,8 +195,8 @@ packages: dev: true optional: true - /@esbuild/sunos-x64@0.17.15: - resolution: {integrity: sha512-U6tYPovOkw3459t2CBwGcFYfFRjivcJJc1WC8Q3funIwX8x4fP+R6xL/QuTPNGOblbq/EUDxj9GU+dWKX0oWlQ==} + /@esbuild/sunos-x64@0.19.12: + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} engines: {node: '>=12'} cpu: [x64] os: [sunos] @@ -594,8 +204,8 @@ packages: dev: true optional: true - /@esbuild/win32-arm64@0.17.15: - resolution: {integrity: sha512-W+Z5F++wgKAleDABemiyXVnzXgvRFs+GVKThSI+mGgleLWluv0D7Diz4oQpgdpNzh4i2nNDzQtWbjJiqutRp6Q==} + /@esbuild/win32-arm64@0.19.12: + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] @@ -603,8 +213,8 @@ packages: dev: true optional: true - /@esbuild/win32-ia32@0.17.15: - resolution: {integrity: sha512-Muz/+uGgheShKGqSVS1KsHtCyEzcdOn/W/Xbh6H91Etm+wiIfwZaBn1W58MeGtfI8WA961YMHFYTthBdQs4t+w==} + /@esbuild/win32-ia32@0.19.12: + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} engines: {node: '>=12'} cpu: [ia32] os: [win32] @@ -612,8 +222,8 @@ packages: dev: true optional: true - /@esbuild/win32-x64@0.17.15: - resolution: {integrity: sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA==} + /@esbuild/win32-x64@0.19.12: + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -621,14 +231,16 @@ packages: dev: true optional: true - /@faker-js/faker@8.0.2: - resolution: {integrity: sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'} - dev: true - - /@istanbuljs/schema@0.1.3: - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 dev: true /@jridgewell/gen-mapping@0.3.3: @@ -636,12 +248,12 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.22 dev: true - /@jridgewell/resolve-uri@3.1.0: - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} engines: {node: '>=6.0.0'} dev: true @@ -650,340 +262,166 @@ packages: engines: {node: '>=6.0.0'} dev: true - /@jridgewell/sourcemap-codec@1.4.14: - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.22: + resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /@jridgewell/trace-mapping@0.3.18: - resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 dev: true - /@next/env@13.4.6: - resolution: {integrity: sha512-nqUxEtvDqFhmV1/awSg0K2XHNwkftNaiUqCYO9e6+MYmqNObpKVl7OgMkGaQ2SZnFx5YqF0t60ZJTlyJIDAijg==} - dev: false + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true - /@next/swc-darwin-arm64@13.4.6: - resolution: {integrity: sha512-ahi6VP98o4HV19rkOXPSUu+ovfHfUxbJQ7VVJ7gL2FnZRr7onEFC1oGQ6NQHpm8CxpIzSSBW79kumlFMOmZVjg==} - engines: {node: '>= 10'} + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.0 + dev: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm-eabi@4.9.6: + resolution: {integrity: sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.9.6: + resolution: {integrity: sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.9.6: + resolution: {integrity: sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==} cpu: [arm64] os: [darwin] requiresBuild: true - dev: false + dev: true optional: true - /@next/swc-darwin-x64@13.4.6: - resolution: {integrity: sha512-13cXxKFsPJIJKzUqrU5XB1mc0xbUgYsRcdH6/rB8c4NMEbWGdtD4QoK9ShN31TZdePpD4k416Ur7p+deMIxnnA==} - engines: {node: '>= 10'} + /@rollup/rollup-darwin-x64@4.9.6: + resolution: {integrity: sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==} cpu: [x64] os: [darwin] requiresBuild: true - dev: false + dev: true optional: true - /@next/swc-linux-arm64-gnu@13.4.6: - resolution: {integrity: sha512-Ti+NMHEjTNktCVxNjeWbYgmZvA2AqMMI2AMlzkXsU7W4pXCMhrryAmAIoo+7YdJbsx01JQWYVxGe62G6DoCLaA==} - engines: {node: '>= 10'} + /@rollup/rollup-linux-arm-gnueabihf@4.9.6: + resolution: {integrity: sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.9.6: + resolution: {integrity: sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==} cpu: [arm64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true - /@next/swc-linux-arm64-musl@13.4.6: - resolution: {integrity: sha512-OHoC6gO7XfjstgwR+z6UHKlvhqJfyMtNaJidjx3sEcfaDwS7R2lqR5AABi8PuilGgi0BO0O0sCXqLlpp3a0emQ==} - engines: {node: '>= 10'} + /@rollup/rollup-linux-arm64-musl@4.9.6: + resolution: {integrity: sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==} cpu: [arm64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true - /@next/swc-linux-x64-gnu@13.4.6: - resolution: {integrity: sha512-zHZxPGkUlpfNJCboUrFqwlwEX5vI9LSN70b8XEb0DYzzlrZyCyOi7hwDp/+3Urm9AB7YCAJkgR5Sp1XBVjHdfQ==} - engines: {node: '>= 10'} + /@rollup/rollup-linux-riscv64-gnu@4.9.6: + resolution: {integrity: sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.9.6: + resolution: {integrity: sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==} cpu: [x64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true - /@next/swc-linux-x64-musl@13.4.6: - resolution: {integrity: sha512-K/Y8lYGTwTpv5ME8PSJxwxLolaDRdVy+lOd9yMRMiQE0BLUhtxtCWC9ypV42uh9WpLjoaD0joOsB9Q6mbrSGJg==} - engines: {node: '>= 10'} + /@rollup/rollup-linux-x64-musl@4.9.6: + resolution: {integrity: sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==} cpu: [x64] os: [linux] requiresBuild: true - dev: false + dev: true optional: true - /@next/swc-win32-arm64-msvc@13.4.6: - resolution: {integrity: sha512-U6LtxEUrjBL2tpW+Kr1nHCSJWNeIed7U7l5o7FiKGGwGgIlFi4UHDiLI6TQ2lxi20fAU33CsruV3U0GuzMlXIw==} - engines: {node: '>= 10'} + /@rollup/rollup-win32-arm64-msvc@4.9.6: + resolution: {integrity: sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==} cpu: [arm64] os: [win32] requiresBuild: true - dev: false + dev: true optional: true - /@next/swc-win32-ia32-msvc@13.4.6: - resolution: {integrity: sha512-eEBeAqpCfhdPSlCZCayjCiyIllVqy4tcqvm1xmg3BgJG0G5ITiMM4Cw2WVeRSgWDJqQGRyyb+q8Y2ltzhXOWsQ==} - engines: {node: '>= 10'} + /@rollup/rollup-win32-ia32-msvc@4.9.6: + resolution: {integrity: sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==} cpu: [ia32] os: [win32] requiresBuild: true - dev: false + dev: true optional: true - /@next/swc-win32-x64-msvc@13.4.6: - resolution: {integrity: sha512-OrZs94AuO3ZS5tnqlyPRNgfWvboXaDQCi5aXGve3o3C+Sj0ctMUV9+Do+0zMvvLRumR8E0PTWKvtz9n5vzIsWw==} - engines: {node: '>= 10'} + /@rollup/rollup-win32-x64-msvc@4.9.6: + resolution: {integrity: sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==} cpu: [x64] os: [win32] requiresBuild: true - dev: false - optional: true - - /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - requiresBuild: true dev: true optional: true - /@remix-run/router@1.6.3: - resolution: {integrity: sha512-EXJysQ7J3veRECd0kZFQwYYd5sJMcq2O/m60zu1W2l3oVQ9xtub8jTOtYRE0+M2iomyG/W3Ps7+vp2kna0C27Q==} - engines: {node: '>=14'} - dev: false - - /@swc/helpers@0.5.1: - resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==} - dependencies: - tslib: 2.5.0 - dev: false - - /@tanstack/query-core@4.29.14: - resolution: {integrity: sha512-ElEAahtLWA7Y7c2Haw10KdEf2tx+XZl/Z8dmyWxZehxWK3TPL5qtXtb7kUEhvt/8u2cSP62fLxgh2qqzMMGnDQ==} - dev: false - - /@tanstack/react-query@4.29.14(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-wh4bd/QIy85YgTDBtj/7/9ZkpRX41QdZuUL8xKoSzuMCukXvAE1/oJ4p0F15lqQq9W3g2pgcbr2Aa+CNvqABhg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - dependencies: - '@tanstack/query-core': 4.29.14 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - use-sync-external-store: 1.2.0(react@18.2.0) - dev: false - - /@types/body-parser@1.19.2: - resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} - dependencies: - '@types/connect': 3.4.35 - '@types/node': 20.3.1 - dev: true - - /@types/chai-subset@1.3.3: - resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} - dependencies: - '@types/chai': 4.3.5 - dev: true - - /@types/chai@4.3.5: - resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} - dev: true - - /@types/connect@3.4.35: - resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} - dependencies: - '@types/node': 20.3.1 - dev: true - - /@types/cookie-parser@1.4.3: - resolution: {integrity: sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==} - dependencies: - '@types/express': 4.17.17 - dev: true - - /@types/cookie@0.4.1: - resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} - dev: false - - /@types/express-serve-static-core@4.17.33: - resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} - dependencies: - '@types/node': 20.3.1 - '@types/qs': 6.9.7 - '@types/range-parser': 1.2.4 - dev: true - - /@types/express@4.17.17: - resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} - dependencies: - '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.17.33 - '@types/qs': 6.9.7 - '@types/serve-static': 1.15.1 - dev: true - - /@types/istanbul-lib-coverage@2.0.4: - resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} - dev: true - - /@types/mime@3.0.1: - resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} - dev: true - - /@types/node@16.18.23: - resolution: {integrity: sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==} - dev: false - - /@types/node@20.3.1: - resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==} - - /@types/prop-types@15.7.5: - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} - - /@types/qs@6.9.7: - resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} - dev: true - - /@types/range-parser@1.2.4: - resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} - dev: true - - /@types/react-dom@18.0.11: - resolution: {integrity: sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==} - dependencies: - '@types/react': 18.2.13 - - /@types/react@18.2.13: - resolution: {integrity: sha512-vJ+zElvi/Zn9cVXB5slX2xL8PZodPCwPRDpittQdw43JR2AJ5k3vKdgJJyneV/cYgIbLQUwXa9JVDvUZXGba+Q==} - dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.3 - csstype: 3.1.2 - - /@types/scheduler@0.16.3: - resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} - - /@types/serve-static@1.15.1: - resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} - dependencies: - '@types/mime': 3.0.1 - '@types/node': 20.3.1 - dev: true - - /@vitejs/plugin-react@3.1.0(vite@4.3.9): - resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^4.1.0-beta.0 - dependencies: - '@babel/core': 7.21.5 - '@babel/plugin-transform-react-jsx-self': 7.21.0(@babel/core@7.21.5) - '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.5) - magic-string: 0.27.0 - react-refresh: 0.14.0 - vite: 4.3.9(@types/node@20.3.1) - transitivePeerDependencies: - - supports-color - dev: true - - /@vitest/coverage-c8@0.32.2(vitest@0.32.2): - resolution: {integrity: sha512-z07kMTN6e4t1jDY4XXU6W1LxCb3V5Rw7KAZId4VM6BCIGLGz1QqwH9UWYWv7LemqQVnARl5CwaDDwVrkcYgwPg==} - peerDependencies: - vitest: '>=0.30.0 <1' - dependencies: - '@ampproject/remapping': 2.2.1 - c8: 7.13.0 - magic-string: 0.30.0 - picocolors: 1.0.0 - std-env: 3.3.2 - vitest: 0.32.2 - dev: true - - /@vitest/expect@0.32.2: - resolution: {integrity: sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q==} - dependencies: - '@vitest/spy': 0.32.2 - '@vitest/utils': 0.32.2 - chai: 4.3.7 - dev: true - - /@vitest/runner@0.32.2: - resolution: {integrity: sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw==} - dependencies: - '@vitest/utils': 0.32.2 - concordance: 5.0.4 - p-limit: 4.0.0 - pathe: 1.1.0 - dev: true - - /@vitest/snapshot@0.32.2: - resolution: {integrity: sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A==} - dependencies: - magic-string: 0.30.0 - pathe: 1.1.0 - pretty-format: 27.5.1 - dev: true - - /@vitest/spy@0.32.2: - resolution: {integrity: sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug==} - dependencies: - tinyspy: 2.1.0 - dev: true - - /@vitest/utils@0.32.2: - resolution: {integrity: sha512-lnJ0T5i03j0IJaeW73hxe2AuVnZ/y1BhhCOuIcl9LIzXnbpXJT9Lrt6brwKHXLOiA7MZ6N5hSJjt0xE1dGNCzQ==} - dependencies: - diff-sequences: 29.4.3 - loupe: 2.3.6 - pretty-format: 27.5.1 - dev: true - - /accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - dev: false - - /acorn-walk@8.2.0: - resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} - engines: {node: '>=0.4.0'} - dev: true - - /acorn@8.8.2: - resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} - engines: {node: '>=0.4.0'} - hasBin: true + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true - /all-contributors-cli@6.26.0: - resolution: {integrity: sha512-HOMfawD0XyNbOvLUn7rOAP5N9RLnbH+Y/9/IoxwPzCmy6srHSFyRMwbpD0H7Tw+1QzdJT8RH7bTe1IZkPhF+NQ==} + /all-contributors-cli@6.26.1: + resolution: {integrity: sha512-Ymgo3FJACRBEd1eE653FD1J/+uD0kqpUNYfr9zNC1Qby0LgbhDBzB3EF6uvkAbYpycStkk41J+0oo37Lc02yEw==} engines: {node: '>=4'} hasBin: true dependencies: - '@babel/runtime': 7.21.5 - async: 3.2.4 + '@babel/runtime': 7.23.9 + async: 3.2.5 chalk: 4.1.2 didyoumean: 1.2.2 inquirer: 7.3.3 json-fixer: 1.6.15 lodash: 4.17.21 - node-fetch: 2.6.7 + node-fetch: 2.7.0 pify: 5.0.0 yargs: 15.4.1 optionalDependencies: @@ -1004,11 +442,9 @@ packages: engines: {node: '>=8'} dev: true - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - dependencies: - color-convert: 1.9.3 + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} dev: true /ansi-styles@4.3.0: @@ -1018,56 +454,39 @@ packages: color-convert: 2.0.1 dev: true - /ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} dev: true - /array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - dev: false - - /assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} dev: true - /async@3.2.4: - resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 dev: true - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} dev: true - /blueimp-md5@2.19.0: - resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} + /async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} dev: true - /body-parser@1.20.1: - resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.11.0 - raw-body: 2.5.1 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: false + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} dev: true /brace-expansion@2.0.1: @@ -1076,46 +495,21 @@ packages: balanced-match: 1.0.2 dev: true - /browserslist@4.21.5: - resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} dependencies: - caniuse-lite: 1.0.30001476 - electron-to-chromium: 1.4.356 - node-releases: 2.0.10 - update-browserslist-db: 1.0.10(browserslist@4.21.5) + fill-range: 7.0.1 dev: true - /busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - dependencies: - streamsearch: 1.1.0 - dev: false - - /bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - dev: false - - /c8@7.13.0: - resolution: {integrity: sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==} - engines: {node: '>=10.12.0'} - hasBin: true + /bundle-require@4.0.2(esbuild@0.19.12): + resolution: {integrity: sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.17' dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@istanbuljs/schema': 0.1.3 - find-up: 5.0.0 - foreground-child: 2.0.0 - istanbul-lib-coverage: 3.2.0 - istanbul-lib-report: 3.0.0 - istanbul-reports: 3.1.5 - rimraf: 3.0.2 - test-exclude: 6.0.0 - v8-to-istanbul: 9.1.0 - yargs: 16.2.0 - yargs-parser: 20.2.9 + esbuild: 0.19.12 + load-tsconfig: 0.2.5 dev: true /cac@6.7.14: @@ -1123,43 +517,11 @@ packages: engines: {node: '>=8'} dev: true - /call-bind@1.0.2: - resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} - dependencies: - function-bind: 1.1.1 - get-intrinsic: 1.2.0 - dev: false - /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} dev: true - /caniuse-lite@1.0.30001476: - resolution: {integrity: sha512-JmpktFppVSvyUN4gsLS0bShY2L9ZUslHLE72vgemBkS43JD2fOvKTKs+GtRwuxrtRGnwJFW0ye7kWRRlLJS9vQ==} - - /chai@4.3.7: - resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} - engines: {node: '>=4'} - dependencies: - assertion-error: 1.1.0 - check-error: 1.0.2 - deep-eql: 4.1.3 - get-func-name: 2.0.0 - loupe: 2.3.6 - pathval: 1.1.1 - type-detect: 4.0.8 - dev: true - - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - dev: true - /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1172,8 +534,19 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true - /check-error@1.0.2: - resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 dev: true /cli-cursor@3.1.0: @@ -1188,10 +561,6 @@ packages: engines: {node: '>= 10'} dev: true - /client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - dev: false - /cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} dependencies: @@ -1200,29 +569,6 @@ packages: wrap-ansi: 6.2.0 dev: true - /cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - dev: true - - /cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - dev: true - - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - dependencies: - color-name: 1.1.3 - dev: true - /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1230,84 +576,13 @@ packages: color-name: 1.1.4 dev: true - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true - /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true - - /concordance@5.0.4: - resolution: {integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==} - engines: {node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14'} - dependencies: - date-time: 3.1.0 - esutils: 2.0.3 - fast-diff: 1.2.0 - js-string-escape: 1.0.1 - lodash: 4.17.21 - md5-hex: 3.0.1 - semver: 7.3.8 - well-known-symbols: 2.0.0 - dev: true - - /content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - dependencies: - safe-buffer: 5.2.1 - dev: false - - /content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - dev: false - - /convert-source-map@1.9.0: - resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - dev: true - - /cookie-parser@1.4.6: - resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} - engines: {node: '>= 0.8.0'} - dependencies: - cookie: 0.4.1 - cookie-signature: 1.0.6 - dev: false - - /cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - dev: false - - /cookie@0.4.1: - resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} - engines: {node: '>= 0.6'} - dev: false - - /cookie@0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} - engines: {node: '>= 0.6'} - dev: false - - /cookies-next@2.1.2: - resolution: {integrity: sha512-czxcfqVaQlo0Q/3xMgp/2jpspsuLJrIm6D37wlmibP3DAcYT315c8UxQmDMohhAT/GRWpaHzpDEFANBjzTFQGg==} - dependencies: - '@types/cookie': 0.4.1 - '@types/node': 16.18.23 - cookie: 0.4.1 - dev: false - - /cross-fetch@3.1.5: - resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} - dependencies: - node-fetch: 2.6.7 - transitivePeerDependencies: - - encoding + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} dev: true /cross-spawn@7.0.3: @@ -1319,27 +594,6 @@ packages: which: 2.0.2 dev: true - /csstype@3.1.2: - resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} - - /date-time@3.1.0: - resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} - engines: {node: '>=6'} - dependencies: - time-zone: 1.0.0 - dev: true - - /debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.0.0 - dev: false - /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -1357,149 +611,80 @@ packages: engines: {node: '>=0.10.0'} dev: true - /deep-eql@4.1.3: - resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} - engines: {node: '>=6'} - dependencies: - type-detect: 4.0.8 - dev: true - - /depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dev: false - - /destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dev: false - /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true - /diff-sequences@29.4.3: - resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 dev: true - /ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - dev: false - - /electron-to-chromium@1.4.356: - resolution: {integrity: sha512-nEftV1dRX3omlxAj42FwqRZT0i4xd2dIg39sog/CnCJeCcL1TRd2Uh0i9Oebgv8Ou0vzTPw++xc+Z20jzS2B6A==} + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} dev: true - /encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - dev: false - - /envalid@7.3.1: - resolution: {integrity: sha512-KL1YRwn8WcoF/Ty7t+yLLtZol01xr9ZJMTjzoGRM8NaSU+nQQjSWOQKKJhJP2P57bpdakJ9jbxqQX4fGTOicZg==} - engines: {node: '>=8.12'} - dependencies: - tslib: 2.3.1 + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true - /esbuild@0.17.15: - resolution: {integrity: sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==} + /esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - '@esbuild/android-arm': 0.17.15 - '@esbuild/android-arm64': 0.17.15 - '@esbuild/android-x64': 0.17.15 - '@esbuild/darwin-arm64': 0.17.15 - '@esbuild/darwin-x64': 0.17.15 - '@esbuild/freebsd-arm64': 0.17.15 - '@esbuild/freebsd-x64': 0.17.15 - '@esbuild/linux-arm': 0.17.15 - '@esbuild/linux-arm64': 0.17.15 - '@esbuild/linux-ia32': 0.17.15 - '@esbuild/linux-loong64': 0.17.15 - '@esbuild/linux-mips64el': 0.17.15 - '@esbuild/linux-ppc64': 0.17.15 - '@esbuild/linux-riscv64': 0.17.15 - '@esbuild/linux-s390x': 0.17.15 - '@esbuild/linux-x64': 0.17.15 - '@esbuild/netbsd-x64': 0.17.15 - '@esbuild/openbsd-x64': 0.17.15 - '@esbuild/sunos-x64': 0.17.15 - '@esbuild/win32-arm64': 0.17.15 - '@esbuild/win32-ia32': 0.17.15 - '@esbuild/win32-x64': 0.17.15 - dev: true - - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 dev: true - /escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - dev: false - /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} dev: true - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 dev: true - /etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - dev: false - - /express@4.18.2: - resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} - engines: {node: '>= 0.10.0'} - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.1 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.5.0 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.2.0 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.1 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.7 - proxy-addr: 2.0.7 - qs: 6.11.0 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.18.0 - serve-static: 1.15.0 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - dev: false - /external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -1509,8 +694,21 @@ packages: tmp: 0.0.33 dev: true - /fast-diff@1.2.0: - resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fastq@1.17.0: + resolution: {integrity: sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==} + dependencies: + reusify: 1.0.4 dev: true /figures@3.2.0: @@ -1520,20 +718,12 @@ packages: escape-string-regexp: 1.0.5 dev: true - /finalhandler@1.2.0: - resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} - engines: {node: '>= 0.8'} + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} dependencies: - debug: 2.6.9 - encodeurl: 1.0.2 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - dev: false + to-regex-range: 5.0.1 + dev: true /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} @@ -1543,117 +733,61 @@ packages: path-exists: 4.0.0 dev: true - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - dev: true - - /foreground-child@2.0.0: - resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} - engines: {node: '>=8.0.0'} - dependencies: - cross-spawn: 7.0.3 - signal-exit: 3.0.7 - dev: true - /foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} dependencies: cross-spawn: 7.0.3 - signal-exit: 4.0.1 + signal-exit: 4.1.0 dev: true - /forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - dev: false - - /fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - dev: false - - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true - - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true dev: true optional: true - /function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: false - - /gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - dev: true - /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} dev: true - /get-func-name@2.0.0: - resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} dev: true - /get-intrinsic@1.2.0: - resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} dependencies: - function-bind: 1.1.1 - has: 1.0.3 - has-symbols: 1.0.3 - dev: false - - /glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - dev: false + is-glob: 4.0.3 + dev: true - /glob@10.2.7: - resolution: {integrity: sha512-jTKehsravOJo8IJxUGfZILnkvVJM/MOfHRs8QcXolVef2zNI9Tqyy5+SeuOAZd3upViEZQLyFpQhYiHLrMUNmA==} + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true dependencies: foreground-child: 3.1.1 - jackspeak: 2.1.1 - minimatch: 9.0.1 - minipass: 5.0.0 - path-scurry: 1.7.0 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 dev: true - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: true - - /globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - dev: true - - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: false - - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 dev: true /has-flag@4.0.0: @@ -1661,48 +795,22 @@ packages: engines: {node: '>=8'} dev: true - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} - dev: false - - /has@1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} - dependencies: - function-bind: 1.1.1 - dev: false - - /html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} dev: true - /http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - dev: false - /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} dependencies: safer-buffer: 2.1.2 - - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - dependencies: - once: 1.4.0 - wrappy: 1.0.2 dev: true - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + dev: true /inquirer@7.3.3: resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} @@ -1723,87 +831,79 @@ packages: through: 2.3.8 dev: true - /ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - dev: false + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} dev: true - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 dev: true - /istanbul-lib-coverage@3.2.0: - resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} - engines: {node: '>=8'} + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} dev: true - /istanbul-lib-report@3.0.0: - resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - dependencies: - istanbul-lib-coverage: 3.2.0 - make-dir: 3.1.0 - supports-color: 7.2.0 dev: true - /istanbul-reports@3.1.5: - resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} - engines: {node: '>=8'} - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.0 + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /jackspeak@2.1.1: - resolution: {integrity: sha512-juf9stUEwUaILepraGOWIJTLwg48bUnBmRqd2ln2Os1sW987zeoj/hzhbvRB95oMuS2ZTpjULmdwHNX4rzZIZw==} + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} dependencies: - cliui: 8.0.1 + '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 dev: true - /js-string-escape@1.0.1: - resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} - engines: {node: '>= 0.8'} - dev: true - - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - /jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true + /joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} dev: true /json-fixer@1.6.15: resolution: {integrity: sha512-TuDuZ5KrgyjoCIppdPXBMqiGfota55+odM+j2cQ5rt/XKyKmqGB3Whz1F8SN8+60yYGy/Nu5lbRZ+rx8kBIvBw==} engines: {node: '>=10'} dependencies: - '@babel/runtime': 7.21.5 + '@babel/runtime': 7.23.9 chalk: 4.1.2 pegjs: 0.10.0 dev: true - /json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true + /lilconfig@3.0.0: + resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} + engines: {node: '>=14'} dev: true - /jsonc-parser@3.2.0: - resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true - /local-pkg@0.4.3: - resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} - engines: {node: '>=14'} + /load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true /locate-path@5.0.0: @@ -1813,211 +913,71 @@ packages: p-locate: 4.1.0 dev: true - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - dependencies: - p-locate: 5.0.0 + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} dev: true /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: true - /loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - dependencies: - js-tokens: 4.0.0 - dev: false - - /loupe@2.3.6: - resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} - dependencies: - get-func-name: 2.0.0 - dev: true - - /lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - dependencies: - yallist: 3.1.1 - dev: true - - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true - - /lru-cache@9.1.1: - resolution: {integrity: sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==} + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} engines: {node: 14 || >=16.14} dev: true - /magic-string@0.27.0: - resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/sourcemap-codec': 1.4.14 - dev: true - - /magic-string@0.30.0: - resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/sourcemap-codec': 1.4.14 + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true - /make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} - dependencies: - semver: 6.3.0 + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} dev: true - /md5-hex@3.0.1: - resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} - engines: {node: '>=8'} + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} dependencies: - blueimp-md5: 2.19.0 + braces: 3.0.2 + picomatch: 2.3.1 dev: true - /media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - dev: false - - /merge-descriptors@1.0.1: - resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} - dev: false - - /methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - dev: false - - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - dev: false - - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - dependencies: - mime-db: 1.52.0 - dev: false - - /mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - dev: false - /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} dev: true - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: true - - /minimatch@9.0.1: - resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 dev: true - /minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - dev: true - - /mlly@1.2.0: - resolution: {integrity: sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==} - dependencies: - acorn: 8.8.2 - pathe: 1.1.0 - pkg-types: 1.0.2 - ufo: 1.1.1 + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} dev: true - /ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - dev: false - /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: false - /mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} dev: true - /nanoid@3.3.6: - resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - /negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - dev: false - - /next@13.4.6(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-sjVqjxU+U2aXZnYt4Ud6CTLNNwWjdSfMgemGpIQJcN3Z7Jni9xRWbR0ie5fQzCg87aLqQVhKA2ud2gPoqJ9lGw==} - engines: {node: '>=16.8.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - fibers: '>= 3.1.0' - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - fibers: - optional: true - sass: - optional: true + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} dependencies: - '@next/env': 13.4.6 - '@swc/helpers': 0.5.1 - busboy: 1.6.0 - caniuse-lite: 1.0.30001476 - postcss: 8.4.14 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(react@18.2.0) - watchpack: 2.4.0 - zod: 3.21.4 - optionalDependencies: - '@next/swc-darwin-arm64': 13.4.6 - '@next/swc-darwin-x64': 13.4.6 - '@next/swc-linux-arm64-gnu': 13.4.6 - '@next/swc-linux-arm64-musl': 13.4.6 - '@next/swc-linux-x64-gnu': 13.4.6 - '@next/swc-linux-x64-musl': 13.4.6 - '@next/swc-win32-arm64-msvc': 13.4.6 - '@next/swc-win32-ia32-msvc': 13.4.6 - '@next/swc-win32-x64-msvc': 13.4.6 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - dev: false + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true - /node-fetch@2.6.7: - resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} peerDependencies: encoding: ^0.1.0 @@ -2028,25 +988,21 @@ packages: whatwg-url: 5.0.0 dev: true - /node-releases@2.0.10: - resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} dev: true - /object-inspect@1.12.3: - resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} - dev: false - - /on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} dependencies: - ee-first: 1.1.1 - dev: false + path-key: 3.1.1 + dev: true - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} dev: true /onetime@5.1.2: @@ -2068,20 +1024,6 @@ packages: p-try: 2.2.0 dev: true - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: true - - /p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - yocto-queue: 1.0.0 - dev: true - /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -2089,56 +1031,32 @@ packages: p-limit: 2.3.0 dev: true - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - dependencies: - p-limit: 3.1.0 - dev: true - /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} dev: true - /parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - dev: false - /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} dev: true - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: true - /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} dev: true - /path-scurry@1.7.0: - resolution: {integrity: sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg==} + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} engines: {node: '>=16 || 14 >=14.17'} dependencies: - lru-cache: 9.1.1 - minipass: 5.0.0 - dev: true - - /path-to-regexp@0.1.7: - resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} - dev: false - - /pathe@1.1.0: - resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==} + lru-cache: 10.2.0 + minipass: 7.0.4 dev: true - /pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} dev: true /pegjs@0.10.0: @@ -2147,136 +1065,63 @@ packages: hasBin: true dev: true - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true /pify@5.0.0: resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} engines: {node: '>=10'} dev: true - /pkg-types@1.0.2: - resolution: {integrity: sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==} - dependencies: - jsonc-parser: 3.2.0 - mlly: 1.2.0 - pathe: 1.1.0 + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} dev: true - /postcss@8.4.14: - resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.6 - picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: false - - /postcss@8.4.23: - resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==} - engines: {node: ^10 || ^12 || >=14} + /postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true dependencies: - nanoid: 3.3.6 - picocolors: 1.0.0 - source-map-js: 1.0.2 + lilconfig: 3.0.0 + yaml: 2.3.4 dev: true /prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true + requiresBuild: true dev: true + optional: true - /pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - dev: true - - /proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - dev: false - - /qs@6.11.0: - resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} - engines: {node: '>=0.6'} - dependencies: - side-channel: 1.0.4 - dev: false - - /range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - dev: false - - /raw-body@2.5.1: - resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} - engines: {node: '>= 0.8'} - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - dev: false - - /react-dom@18.2.0(react@18.2.0): - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} - peerDependencies: - react: ^18.2.0 - dependencies: - loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.0 - dev: false - - /react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} dev: true - /react-refresh@0.14.0: - resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} - engines: {node: '>=0.10.0'} + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true - /react-router-dom@6.13.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-6Nqoqd7fgwxxVGdbiMHTpDHCYPq62d7Wk1Of7B82vH7ZPwwsRaIa22zRZKPPg413R5REVNiyuQPKDG1bubcOFA==} - engines: {node: '>=14'} - peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' - dependencies: - '@remix-run/router': 1.6.3 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-router: 6.13.0(react@18.2.0) - dev: false - - /react-router@6.13.0(react@18.2.0): - resolution: {integrity: sha512-Si6KnfEnJw7gUQkNa70dlpI1bul46FuSxX5t5WwlUBxE25DAz2BjVkwaK8Y2s242bQrZPXCpmwLPtIO5pv4tXg==} - engines: {node: '>=14'} - peerDependencies: - react: '>=16.8' - dependencies: - '@remix-run/router': 1.6.3 - react: 18.2.0 - dev: false - - /react@18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} dependencies: - loose-envify: 1.4.0 - dev: false + picomatch: 2.3.1 + dev: true - /regenerator-runtime@0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} dev: true /require-directory@2.1.1: @@ -2288,6 +1133,11 @@ packages: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} dev: true + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + /restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -2296,27 +1146,32 @@ packages: signal-exit: 3.0.7 dev: true - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - dependencies: - glob: 7.2.3 + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: true - /rimraf@5.0.1: - resolution: {integrity: sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==} - engines: {node: '>=14'} + /rollup@4.9.6: + resolution: {integrity: sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true dependencies: - glob: 10.2.7 - dev: true - - /rollup@3.21.2: - resolution: {integrity: sha512-c4vC+JZ3bbF4Kqq2TtM7zSKtSyMybFOjqmomFax3xpfYaPZDZ4iz8NMIuBRMjnXOcKYozw7bC6vhJjiWD6JpzQ==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} - hasBin: true + '@types/estree': 1.0.5 optionalDependencies: - fsevents: 2.3.2 + '@rollup/rollup-android-arm-eabi': 4.9.6 + '@rollup/rollup-android-arm64': 4.9.6 + '@rollup/rollup-darwin-arm64': 4.9.6 + '@rollup/rollup-darwin-x64': 4.9.6 + '@rollup/rollup-linux-arm-gnueabihf': 4.9.6 + '@rollup/rollup-linux-arm64-gnu': 4.9.6 + '@rollup/rollup-linux-arm64-musl': 4.9.6 + '@rollup/rollup-linux-riscv64-gnu': 4.9.6 + '@rollup/rollup-linux-x64-gnu': 4.9.6 + '@rollup/rollup-linux-x64-musl': 4.9.6 + '@rollup/rollup-win32-arm64-msvc': 4.9.6 + '@rollup/rollup-win32-ia32-msvc': 4.9.6 + '@rollup/rollup-win32-x64-msvc': 4.9.6 + fsevents: 2.3.3 dev: true /run-async@2.4.1: @@ -2324,6 +1179,12 @@ packages: engines: {node: '>=0.12.0'} dev: true + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + /rxjs@6.6.7: resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} engines: {npm: '>=2.0.0'} @@ -2331,73 +1192,14 @@ packages: tslib: 1.14.1 dev: true - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: false - /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - /scheduler@0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} - dependencies: - loose-envify: 1.4.0 - dev: false - - /semver@6.3.0: - resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} - hasBin: true dev: true - /semver@7.3.8: - resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - - /send@0.18.0: - resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} - engines: {node: '>= 0.8.0'} - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - dev: false - - /serve-static@1.15.0: - resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} - engines: {node: '>= 0.8.0'} - dependencies: - encodeurl: 1.0.2 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.18.0 - transitivePeerDependencies: - - supports-color - dev: false - /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: true - /setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - dev: false - /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2410,49 +1212,27 @@ packages: engines: {node: '>=8'} dev: true - /side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.0 - object-inspect: 1.12.3 - dev: false - - /siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - dev: true - /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true - /signal-exit@4.0.1: - resolution: {integrity: sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw==} + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} dev: true - /source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - - /stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} dev: true - /statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - dev: false - - /std-env@3.3.2: - resolution: {integrity: sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==} + /source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 dev: true - /streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - dev: false - /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2462,6 +1242,15 @@ packages: strip-ansi: 6.0.1 dev: true + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2469,34 +1258,30 @@ packages: ansi-regex: 5.0.1 dev: true - /strip-literal@1.0.1: - resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} dependencies: - acorn: 8.8.2 + ansi-regex: 6.0.1 dev: true - /styled-jsx@5.1.1(react@18.2.0): - resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - dependencies: - client-only: 0.0.1 - react: 18.2.0 - dev: false + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + /sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true dependencies: - has-flag: 3.0.0 + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 10.3.10 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 dev: true /supports-color@7.2.0: @@ -2506,36 +1291,21 @@ packages: has-flag: 4.0.0 dev: true - /test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 - dev: true - - /through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: true - - /time-zone@1.0.0: - resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==} - engines: {node: '>=4'} - dev: true - - /tinybench@2.5.0: - resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} + thenify: 3.3.1 dev: true - /tinypool@0.5.0: - resolution: {integrity: sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==} - engines: {node: '>=14.0.0'} + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 dev: true - /tinyspy@2.1.0: - resolution: {integrity: sha512-7eORpyqImoOvkQJCSkL0d0mB4NHHIFAy4b1u8PHdDa7SjGS2njzl6/lyGoZLm+eyYEtlUmFGE0rFj66SWxZgQQ==} - engines: {node: '>=14.0.0'} + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: true /tmp@0.0.33: @@ -2545,248 +1315,92 @@ packages: os-tmpdir: 1.0.2 dev: true - /to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 dev: true - /toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - dev: false - /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: true - /tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: true - - /tslib@2.3.1: - resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} - dev: true - - /tslib@2.5.0: - resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} - dev: false - - /type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - dev: true - - /type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - dev: true - - /type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - dev: false - - /typescript@5.1.3: - resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==} - engines: {node: '>=14.17'} - hasBin: true - - /ufo@1.1.1: - resolution: {integrity: sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==} - dev: true - - /unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - dev: false - - /update-browserslist-db@1.0.10(browserslist@4.21.5): - resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - dependencies: - browserslist: 4.21.5 - escalade: 3.1.1 - picocolors: 1.0.0 - dev: true - - /use-sync-external-store@1.2.0(react@18.2.0): - resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - react: 18.2.0 - dev: false - - /utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - dev: false - - /v8-to-istanbul@9.1.0: - resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} - engines: {node: '>=10.12.0'} + /tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} dependencies: - '@jridgewell/trace-mapping': 0.3.18 - '@types/istanbul-lib-coverage': 2.0.4 - convert-source-map: 1.9.0 + punycode: 2.3.1 dev: true - /vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - dev: false - - /vite-node@0.32.2(@types/node@20.3.1): - resolution: {integrity: sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA==} - engines: {node: '>=v14.18.0'} + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - dependencies: - cac: 6.7.14 - debug: 4.3.4 - mlly: 1.2.0 - pathe: 1.1.0 - picocolors: 1.0.0 - vite: 4.3.9(@types/node@20.3.1) - transitivePeerDependencies: - - '@types/node' - - less - - sass - - stylus - - sugarss - - supports-color - - terser dev: true - /vite@4.3.9(@types/node@20.3.1): - resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - '@types/node': '>= 14' - less: '*' - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - '@types/node': 20.3.1 - esbuild: 0.17.15 - postcss: 8.4.23 - rollup: 3.21.2 - optionalDependencies: - fsevents: 2.3.2 + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true - /vitest-fetch-mock@0.2.2(vitest@0.32.2): - resolution: {integrity: sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==} - engines: {node: '>=14.14.0'} - peerDependencies: - vitest: '>=0.16.0' - dependencies: - cross-fetch: 3.1.5 - vitest: 0.32.2 - transitivePeerDependencies: - - encoding + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true - /vitest@0.32.2: - resolution: {integrity: sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ==} - engines: {node: '>=v14.18.0'} + /tsup@8.0.1(typescript@5.3.3): + resolution: {integrity: sha512-hvW7gUSG96j53ZTSlT4j/KL0q1Q2l6TqGBFc6/mu/L46IoNWqLLUzLRLP1R8Q7xrJTmkDxxDoojV5uCVs1sVOg==} + engines: {node: '>=18'} hasBin: true peerDependencies: - '@edge-runtime/vm': '*' - '@vitest/browser': '*' - '@vitest/ui': '*' - happy-dom: '*' - jsdom: '*' - playwright: '*' - safaridriver: '*' - webdriverio: '*' + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: + '@microsoft/api-extractor': optional: true - jsdom: + '@swc/core': optional: true - playwright: + postcss: optional: true - safaridriver: - optional: true - webdriverio: + typescript: optional: true dependencies: - '@types/chai': 4.3.5 - '@types/chai-subset': 1.3.3 - '@types/node': 20.3.1 - '@vitest/expect': 0.32.2 - '@vitest/runner': 0.32.2 - '@vitest/snapshot': 0.32.2 - '@vitest/spy': 0.32.2 - '@vitest/utils': 0.32.2 - acorn: 8.8.2 - acorn-walk: 8.2.0 + bundle-require: 4.0.2(esbuild@0.19.12) cac: 6.7.14 - chai: 4.3.7 - concordance: 5.0.4 + chokidar: 3.5.3 debug: 4.3.4 - local-pkg: 0.4.3 - magic-string: 0.30.0 - pathe: 1.1.0 - picocolors: 1.0.0 - std-env: 3.3.2 - strip-literal: 1.0.1 - tinybench: 2.5.0 - tinypool: 0.5.0 - vite: 4.3.9(@types/node@20.3.1) - vite-node: 0.32.2(@types/node@20.3.1) - why-is-node-running: 2.2.2 + esbuild: 0.19.12 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 4.0.2 + resolve-from: 5.0.0 + rollup: 4.9.6 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tree-kill: 1.2.2 + typescript: 5.3.3 transitivePeerDependencies: - - less - - sass - - stylus - - sugarss - supports-color - - terser + - ts-node dev: true - /watchpack@2.4.0: - resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} - engines: {node: '>=10.13.0'} - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - dev: false + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: true - /well-known-symbols@2.0.0: - resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==} - engines: {node: '>=6'} + /webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true /whatwg-url@5.0.0: @@ -2796,6 +1410,14 @@ packages: webidl-conversions: 3.0.1 dev: true + /whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: true + /which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} dev: true @@ -2808,15 +1430,6 @@ packages: isexe: 2.0.0 dev: true - /why-is-node-running@2.2.2: - resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} - engines: {node: '>=8'} - hasBin: true - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - dev: true - /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -2835,25 +1448,22 @@ packages: strip-ansi: 6.0.1 dev: true - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 dev: true /y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} dev: true - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: true - - /yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: true - - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} dev: true /yargs-parser@18.1.3: @@ -2864,11 +1474,6 @@ packages: decamelize: 1.2.0 dev: true - /yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - dev: true - /yargs@15.4.1: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} engines: {node: '>=8'} @@ -2885,30 +1490,3 @@ packages: y18n: 4.0.3 yargs-parser: 18.1.3 dev: true - - /yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - dependencies: - cliui: 7.0.4 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 - dev: true - - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true - - /yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} - engines: {node: '>=12.20'} - dev: true - - /zod@3.21.4: - resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} - dev: false diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 1a0bb63..0000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -packages: - - "examples/**" - - "." diff --git a/src/album/album.base.schemas.ts b/src/album/album.base.schemas.ts new file mode 100644 index 0000000..1707e57 --- /dev/null +++ b/src/album/album.base.schemas.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; +import { + externalUrlsSchema, + imageSchema, + releaseDatePrecisionEnum, + restrictionsSchema, +} from "../general.shemas.ts"; +import { simplifiedArtistSchema } from "../artist/artist.schemas.ts"; + +export const albumTypeEnum = z.enum([ + "single", + "album", + "compilation", + "ep", +]); +export const albumGroupEnum = z.enum([ + ...albumTypeEnum._def.values, + "appears_on", +]); + +export const albumBaseSchema = z.object({ + album_type: albumTypeEnum, + total_tracks: z.number().positive(), + available_markets: z.array(z.string()).optional(), + external_urls: externalUrlsSchema, + href: z.string().url(), + id: z.string(), + images: z.array(imageSchema), + name: z.string(), + release_date: z.string(), + release_date_precision: releaseDatePrecisionEnum, + restrictions: restrictionsSchema.optional(), + type: z.literal("album"), + uri: z.string(), + // not sure about that one + is_playable: z.boolean().optional(), +}).strict(); + +export const simplifiedAlbumSchema = z.object({ + album_group: albumGroupEnum.optional(), + artists: z.array(simplifiedArtistSchema), +}).merge(albumBaseSchema).strict(); diff --git a/src/album/album.endpoints.ts b/src/album/album.endpoints.ts new file mode 100644 index 0000000..5f0418e --- /dev/null +++ b/src/album/album.endpoints.ts @@ -0,0 +1,200 @@ +import type { HTTPClient } from "../client.ts"; +import type { PagingObject, PagingOptions } from "../general.types.ts"; +import type { SimplifiedTrack } from "../track/track.types.ts"; +import type { Album, SavedAlbum, SimplifiedAlbum } from "./album.types.ts"; +import type { Prettify } from "../shared.ts"; + +/** + * Get Spotify catalog information for a single album. + * + * @param client Spotify HTTPClient + * @param albumId The Spotify ID of the album + * @param market An ISO 3166-1 alpha-2 country code + */ +export const getAlbum = async ( + client: HTTPClient, + albumId: string, + market?: string, +) => { + const res = await client.fetch("/v1/albums/" + albumId, { + query: { market }, + }); + return res.json() as Promise; +}; + +/** + * Get Spotify catalog information for multiple albums identified by their Spotify IDs. + * + * @param client Spotify HTTPClient + * @param albumIds List of the Spotify IDs for the albums. Maximum: 20 IDs + * @param market An ISO 3166-1 alpha-2 country code + */ +export const getAlbums = async ( + client: HTTPClient, + albumIds: string[], + market?: string, +) => { + const res = await client.fetch("/v1/albums", { + query: { market, ids: albumIds }, + }); + return (await res.json() as { albums: Album[] }).albums; +}; + +export type GetAlbumTrackOpts = Prettify< + PagingOptions & { + /** + * An ISO 3166-1 alpha-2 country code. + * If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + } +>; + +/** + * Get Spotify catalog information about an album’s tracks. + * Optional parameters can be used to limit the number of tracks returned. + * + * @param client Spotify HTTPClient + * @param albumId The Spotify ID of the album + * @param options Additional option for request + */ +export const getAlbumTracks = async ( + client: HTTPClient, + albumId: string, + options?: GetAlbumTrackOpts, +) => { + const res = await client.fetch(`/v1/albums/${albumId}/tracks`, { + query: options, + }); + return res.json() as Promise>; +}; + +export type GetSavedAlbumsOpts = Prettify< + PagingOptions & { + /** + * An ISO 3166-1 alpha-2 country code. + * If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + } +>; + +/** + * Get a list of the albums saved in the current Spotify user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param options Additional option for request + */ +export const getSavedAlbums = async ( + client: HTTPClient, + options?: GetSavedAlbumsOpts, +) => { + const res = await client.fetch("/v1/me/albums", { query: options }); + return res.json() as Promise>; +}; + +/** + * Save one or more albums to the current user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param albumIds List of the Spotify IDs for the albums. Maximum: 20 IDs + */ +export const saveAlbums = (client: HTTPClient, albumIds: string[]) => { + return client.fetch("/v1/me/albums", { + method: "PUT", + query: { ids: albumIds }, + }); +}; + +/** + * Save album to the current user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param albums_id The Spotify ID of the album + */ +export const saveAlbum = (client: HTTPClient, albumId: string) => { + return saveAlbums(client, [albumId]); +}; + +/** + * Remove one or more albums from the current user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param albumIds List of the Spotify IDs for the albums. Maximum: 20 IDs + */ +export const removeSavedAlbums = ( + client: HTTPClient, + albumIds: string[], +) => { + return client.fetch("/v1/me/albums", { + method: "DELETE", + query: { ids: albumIds }, + }); +}; + +/** + * Remove album from the current user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param albumId The Spotify ID of the album + */ +export const removeSavedAlbum = ( + client: HTTPClient, + albumId: string, +) => { + return removeSavedAlbums(client, [albumId]); +}; + +/** + * Check if one or more albums is already saved in the current Spotify user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param albumIds List of the Spotify IDs for the albums. Maximum: 20 IDs + */ +export const checkIfAlbumsSaved = async ( + client: HTTPClient, + albumIds: string[], +) => { + const res = await client.fetch("/v1/me/albums/contains", { + query: { ids: albumIds }, + }); + return res.json() as Promise; +}; + +/** + * Check if album is already saved in the current Spotify user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param albumId The Spotify ID of the album + */ +export const checkIfAlbumSaved = async ( + client: HTTPClient, + albumId: string, +) => { + return (await checkIfAlbumsSaved(client, [albumId]))[0]!; +}; + +export type GetNewReleasesOpts = Prettify< + PagingOptions & { + /** + * An ISO 3166-1 alpha-2 country code. + * Provide this parameter if you want the list of returned items to be relevant to a particular country. + * If omitted, the returned items will be relevant to all countries. + */ + country?: string; + } +>; + +/** + * Get a list of new album releases featured in Spotify (shown, for example, on a Spotify player’s “Browse” tab). + * + * @param client Spotify HTTPClient + * @param options Additional option for request + */ +export const getNewAlbumReleases = async ( + client: HTTPClient, + options?: GetNewReleasesOpts, +) => { + const res = await client.fetch("/v1/browse/new-releases", { query: options }); + return (await res.json() as { albums: PagingObject }).albums; +}; diff --git a/src/album/album.schemas.ts b/src/album/album.schemas.ts new file mode 100644 index 0000000..849c02e --- /dev/null +++ b/src/album/album.schemas.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; +import { + copyrightSchema, + externalIdsSchema, + pagingObjectSchema, +} from "../general.shemas.ts"; +import { simplifiedArtistSchema } from "../artist/artist.schemas.ts"; +import { simplifiedTrackSchema } from "../track/track.schemas.ts"; +import { albumBaseSchema } from "./album.base.schemas.ts"; + +export const albumSchema = z.object({ + artists: z.array(simplifiedArtistSchema), + tracks: pagingObjectSchema(simplifiedTrackSchema), + external_ids: externalIdsSchema, + copyrights: z.array(copyrightSchema), + genres: z.array(z.string()), + label: z.string(), + popularity: z.number().min(0).max(100), +}).merge(albumBaseSchema).strict(); + +export const savedAlbumSchema = z.object({ + added_at: z.string(), + album: albumSchema, +}); diff --git a/src/album/album.test.ts b/src/album/album.test.ts new file mode 100644 index 0000000..14a9ada --- /dev/null +++ b/src/album/album.test.ts @@ -0,0 +1,77 @@ +import { albumSchema, savedAlbumSchema } from "./album.schemas.ts"; +import { client } from "../test_client.ts"; +import { + getAlbum, + getAlbums, + getNewAlbumReleases, + getSavedAlbums, +} from "./album.endpoints.ts"; +import { + Album, + AlbumGroup, + AlbumType, + SavedAlbum, + SimplifiedAlbum, +} from "./album.types.ts"; +import { z } from "zod"; +import { pagingObjectSchema } from "../general.shemas.ts"; +import { AssertTrue, IsExact } from "std/testing/types.ts"; +import { simplifiedAlbumSchema } from "./album.base.schemas.ts"; +import { albumGroupEnum } from "./album.base.schemas.ts"; +import { albumTypeEnum } from "./album.base.schemas.ts"; + +const MOCK_ALBUM_IDS = [ + "35UJLpClj5EDrhpNIi4DFg", // Radiohead - The Bends + "7FqHuAvmREiIwVXVpZ9ooP", // Bring Me The Horizon - That's The Spirit + "621cXqrTSSJi1WqDMSLmbL", // Twenty One Pilots - Trench + "5U0pevIOTrPoDsN8YsBCBh", // Korn - Issues + "3p7m1Pmg6n3BlpL9Py7IUA", // Bad Omens - THE DEATH OF PEACE OF MIND + "5Eevxp2BCbWq25ZdiXRwYd", // Linkin Park - One More Light + "0FZK97MXMm5mUQ8mtudjuK", // My Chemical Romance - The Black Parade + "5b2m10WqNvZaD8eTEXGyfl", // Palaye Royale - The Bastards + "4w3NeXtywU398NYW4903rY", // Queens of the Stone Age - Songs For The Deaf + "2WT1pbYjLJciAR26yMebkH", // Pink Floyd - The Dark Side Of The Moon +]; + +const getRandomAlbumId = () => { + const randomIndex = Math.floor(Math.random() * MOCK_ALBUM_IDS.length); + return MOCK_ALBUM_IDS[randomIndex]; +}; + +Deno.test("getAlbum", async () => { + const album = await getAlbum(client, getRandomAlbumId()); + albumSchema.parse(album); +}); + +Deno.test("getAlbums", async () => { + const albums = await getAlbums(client, MOCK_ALBUM_IDS); + z.array(albumSchema).parse(albums); +}); + +Deno.test("getSavedAlbums", async () => { + const savedAlbumsPage = await getSavedAlbums(client, { limit: 10 }); + pagingObjectSchema(savedAlbumSchema).parse(savedAlbumsPage); +}); + +Deno.test("getNewAlbumReleases", async () => { + const newAlbumsPage = await getNewAlbumReleases(client, { limit: 10 }); + pagingObjectSchema(simplifiedAlbumSchema).parse(newAlbumsPage); +}); + +Deno.test("albumTypes", () => { + type _t1 = AssertTrue< + IsExact, SimplifiedAlbum> + >; + type _t2 = AssertTrue< + IsExact, Album> + >; + type _t3 = AssertTrue< + IsExact, SavedAlbum> + >; + type _t4 = AssertTrue< + IsExact, AlbumGroup> + >; + type _t5 = AssertTrue< + IsExact, AlbumType> + >; +}); diff --git a/src/album/album.types.ts b/src/album/album.types.ts new file mode 100644 index 0000000..347ac52 --- /dev/null +++ b/src/album/album.types.ts @@ -0,0 +1,116 @@ +import type { + Copyright, + ExternalIds, + ExternalUrls, + Image, + PagingObject, + ReleaseDatePrecision, + Restrictions, +} from "../general.types.ts"; +import type { SimplifiedArtist } from "../artist/artist.types.ts"; +import type { SimplifiedTrack } from "../track/track.types.ts"; + +export type AlbumType = "single" | "album" | "compilation" | "ep"; +/** + * **The field is present when getting an artist's albums.** \ + * Compare to `album_type` this field represents relationship between the artist and the album. + */ +export type AlbumGroup = AlbumType | "appears_on"; + +interface AlbumBase { + album_type: AlbumType; + /** + * The number of tracks in the album. + */ + total_tracks: number; + /** + * The markets in which the album is available: + * ISO 3166-1 alpha-2 country codes. + */ + available_markets?: string[]; + external_urls: ExternalUrls; + /** + * A link to the Web API endpoint providing full details of the album. + */ + href: string; + /** + * The Spotify ID for the album. + */ + id: string; + /** + * The cover art for the album in various sizes, widest first. + */ + images: Image[]; + /** + * The name of the album. + * + * In case of an album takedown, the value may be an empty string. + */ + name: string; + /** + * The date the album was first released. + */ + release_date: string; + /** + * The precision with which `release_date` value is known. + */ + release_date_precision: ReleaseDatePrecision; + /** + * Included in the response when a content restriction is applied. + */ + restrictions?: Restrictions; + type: "album"; + /** + * The Spotify URI for the album. + */ + uri: string; + is_playable?: boolean; +} + +export interface SimplifiedAlbum extends AlbumBase { + /** + * **The field is present when getting an artist's albums.** \ + * Compare to `album_type` this field represents relationship between the artist and the album. + */ + album_group?: AlbumGroup; + /** + * The artists of the album. + */ + artists: SimplifiedArtist[]; +} + +export interface Album extends AlbumBase { + /** + * Known external IDs for the track. + */ + external_ids: ExternalIds; + /** + * The copyright statements of the album. + */ + copyrights: Copyright[]; + /** + * A list of the genres the album is associated with. + * If not yet classified, the array is empty. + */ + genres: string[]; + /** + * The label associated with the album. + */ + label: string; + /** + * The popularity of the artist. + * The value will be between 0 and 100, with 100 being the most popular. + */ + popularity: number; + artists: SimplifiedArtist[]; + tracks: PagingObject; +} + +export type SavedAlbum = { + /** + * The date and time the album was saved. + * Timestamps are returned in ISO 8601 format as Coordinated Universal Time (UTC) with a zero offset: YYYY-MM-DDTHH:MM:SSZ. + */ + added_at: string; + album: Album; +}; diff --git a/src/api/album/album.endpoints.ts b/src/api/album/album.endpoints.ts deleted file mode 100644 index e6692c6..0000000 --- a/src/api/album/album.endpoints.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { HTTPClient } from "../client"; -import { PagingObject, PagingOptions } from "../general.types"; -import { Market } from "../market/market.types"; -import { TrackSimplified } from "../track/track.types"; -import { Album, AlbumSimplified } from "../album/album.types"; - -/** - * Get Spotify catalog information for a single album. - * - * @param client Spotify HTTPClient - * @param album_id The Spotify ID of the album - * @param market An ISO 3166-1 alpha-2 country code - */ -export const getAlbum = async ( - client: HTTPClient, - album_id: string, - market?: Market -) => { - return await client.fetch("/albums/" + album_id, "json", { - query: { market } - }); -}; - -/** - * Get Spotify catalog information for multiple albums identified by their Spotify IDs. - * - * @param client Spotify HTTPClient - * @param album_ids List of the Spotify IDs for the albums. Maximum: 20 IDs - * @param market An ISO 3166-1 alpha-2 country code - */ -export const getAlbums = async ( - client: HTTPClient, - album_ids: string[], - market?: Market -) => { - return ( - await client.fetch<{ albums: Album[] }>("/albums", "json", { - query: { market, ids: album_ids } - }) - ).albums; -}; - -export interface GetAlbumTrackOpts extends PagingOptions { - /** - * An ISO 3166-1 alpha-2 country code. - * If a country code is specified, only content that is available in that market will be returned. - */ - market?: Market; -} - -/** - * Get Spotify catalog information about an album’s tracks. - * Optional parameters can be used to limit the number of tracks returned. - * - * @param client Spotify HTTPClient - * @param album_id The Spotify ID of the album - * @param opts Additional option for request - */ -export const getAlbumTracks = async ( - client: HTTPClient, - album_id: string, - opts?: GetAlbumTrackOpts -) => { - return await client.fetch>( - `/albums/${album_id}/tracks`, - "json", - { - query: opts - } - ); -}; - -export interface GetSavedAlbumsOpts extends PagingOptions { - /** - * An ISO 3166-1 alpha-2 country code. - * If a country code is specified, only content that is available in that market will be returned. - */ - market?: Market; -} - -/** - * Get a list of the albums saved in the current Spotify user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param opts Additional option for request - */ -export const getSavedAlbums = async ( - client: HTTPClient, - opts?: GetSavedAlbumsOpts -) => { - return await client.fetch< - PagingObject<{ - /** - * The date and time the album was saved Timestamps are returned in ISO 8601 format as Coordinated Universal Time (UTC) with a zero offset: YYYY-MM-DDTHH:MM:SSZ. - */ - added_at: string; - /** - * Information about the album. - */ - album: Album; - }> - >("/me/albums", "json", { - query: opts - }); -}; - -/** - * Save one or more albums to the current user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param albums_ids List of the Spotify IDs for the albums. Maximum: 20 IDs - */ -export const saveAlbums = async (client: HTTPClient, albums_ids: string[]) => { - await client.fetch("/me/albums", "void", { - method: "PUT", - query: { ids: albums_ids } - }); -}; - -/** - * Save album to the current user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param albums_id The Spotify ID of the album - */ -export const saveAlbum = async (client: HTTPClient, album_id: string) => { - await saveAlbums(client, [album_id]); -}; - -/** - * Remove one or more albums from the current user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param album_ids List of the Spotify IDs for the albums. Maximum: 20 IDs - */ -export const removeSavedAlbums = async ( - client: HTTPClient, - album_ids: string[] -) => { - await client.fetch("/me/albums", "void", { - method: "DELETE", - query: { - ids: album_ids - } - }); -}; - -/** - * Remove album from the current user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param album_id The Spotify ID of the album - */ -export const removeSavedAlbum = async ( - client: HTTPClient, - album_id: string -) => { - await removeSavedAlbums(client, [album_id]); -}; - -/** - * Check if one or more albums is already saved in the current Spotify user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param album_ids List of the Spotify IDs for the albums. Maximum: 20 IDs - */ -export const checkSavedAlbums = async ( - client: HTTPClient, - album_ids: string[] -) => { - return await client.fetch("/me/albums/contains", "json", { - query: { - ids: album_ids - } - }); -}; - -/** - * Check if album is already saved in the current Spotify user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param album_id The Spotify ID of the album - */ -export const checkSavedAlbum = async (client: HTTPClient, album_id: string) => { - return (await checkSavedAlbums(client, [album_id]))[0]; -}; - -export interface GetNewReleasesOpts extends PagingOptions { - /** - * An ISO 3166-1 alpha-2 country code. - * Provide this parameter if you want the list of returned items to be relevant to a particular country. - * If omitted, the returned items will be relevant to all countries. - */ - country?: Market; -} - -/** - * Get a list of new album releases featured in Spotify (shown, for example, on a Spotify player’s “Browse” tab). - * - * @param client Spotify HTTPClient - * @param opts Additional option for request - */ -export const getNewAlbumReleases = async ( - client: HTTPClient, - opts?: GetNewReleasesOpts -) => { - return ( - await client.fetch<{ albums: PagingObject }>( - "/browse/new-releases", - "json", - { - query: opts - } - ) - ).albums; -}; diff --git a/src/api/album/album.types.ts b/src/api/album/album.types.ts deleted file mode 100644 index 47baf6c..0000000 --- a/src/api/album/album.types.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { - Copyright, - ExternalIds, - ExternalUrls, - Image, - PagingObject, - RestrictionsReason -} from "../general.types"; -import { Artist, ArtistSimplified } from "../artist/artist.types"; -import { Market } from "../market/market.types"; -import { TrackSimplified } from "../track/track.types"; -import { Genre } from "../genre/genre.types"; -import { JSONObject } from "../../shared"; - -/** - * The types of album. - */ -export type AlbumType = "single" | "album" | "compilation"; - -/** - * The groups of album. - */ -export type AlbumGroup = AlbumType | "appears_on"; - -interface AlbumBase extends JSONObject { - /** - * The type of the album. - */ - album_type: AlbumType; - /** - * The number of tracks in the album. - */ - total_tracks: number; - /** - * The markets in which the album is available: - * ISO 3166-1 alpha-2 country codes. - */ - available_markets: Market[]; - /** - * Known external URLs for this album. - */ - external_urls: ExternalUrls; - /** - * A link to the Web API endpoint providing full details of the album. - */ - href: string; - /** - * The Spotify ID for the album. - */ - id: string; - /** - * The cover art for the album in various sizes, widest first. - */ - images: Image[]; - /** - * The name of the album. - * - * In case of an album takedown, the value may be an empty string. - */ - name: string; - /** - * The date the album was first released. - */ - release_date: string; - /** - * The precision with which `release_date` value is known. - */ - release_date_precision: "year" | "month" | "day"; - /** - * Included in the response when a content restriction is applied. - */ - restrictions?: { - /** - * The reason for the restriction. - * - * Albums may be restricted if the content is not available in a given market, to the user's subscription type, or when the user's account is set to not play explicit content. - */ - reason: RestrictionsReason; - }; - /** - * The object type. - */ - type: "album"; - /** - * The Spotify URI for the album. - */ - uri: string; - /** - * Known external IDs for the track. - */ - external_ids?: ExternalIds; - /** - * The copyright statements of the album. - */ - copyrights?: Copyright[]; - /** - * A list of the genres the album is associated with. - * If not yet classified, the array is empty. - */ - genres?: Genre[]; - /** - * The label associated with the album. - */ - label?: string; - /** - * The popularity of the artist. - * The value will be between 0 and 100, with 100 being the most popular. - */ - popularity?: number; -} - -export interface AlbumSimplified extends AlbumBase, JSONObject { - /** - * **The field is present when getting an artist's albums.** - * Compare to `album_type` this field represents relationship between the artist and the album. - */ - album_group?: AlbumGroup; - /** - * The artists of the album. - */ - artists: ArtistSimplified[]; -} - -export interface Album extends AlbumBase, JSONObject { - /** - * The artists of the album. - */ - artists: Artist[]; - /** - * The tracks of the album. - */ - tracks: PagingObject; -} diff --git a/src/api/album/index.ts b/src/api/album/index.ts deleted file mode 100644 index 4d2fc78..0000000 --- a/src/api/album/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./album.endpoints"; -export * from "./album.types"; diff --git a/src/api/api.ts b/src/api/api.ts deleted file mode 100644 index 5bb8e45..0000000 --- a/src/api/api.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as endpoints from "./endpoints"; -import { HTTPClient, SpotifyClient, SpotifyClientOpts } from "./client"; -import { IAuthProvider } from "../shared"; - -import { SearchEndpoint } from "./search/search.endpoints"; -import { UserTopItemsEndpoint } from "./user/user.endpoints"; - -// We separate endpoints with generics because we can't access function -// generics from another type or somehow copy them to another function. -// At least I couldn't do that. :) If you know it, open the issue. -type EndpointsWithGeneric = SearchEndpoint & UserTopItemsEndpoint; -type EndpointNamesWithGeneric = keyof EndpointsWithGeneric; - -type OmitFirst = T extends [unknown, ...infer R] - ? R - : never; - -type EndpointName = keyof typeof endpoints; - -export type ISpoitfyAPI = EndpointsWithGeneric & { - [K in Exclude]: ( - ...args: OmitFirst> - ) => ReturnType<(typeof endpoints)[K]>; -}; - -type Endpoint = (client: HTTPClient, ...args: any[]) => Promise; - -/** - * Assings endpoint functions to client and binds client to each of them - */ -export const createSpotifyAPI = < - T extends IAuthProvider | string = IAuthProvider | string ->( - authProvider: T, - opts?: SpotifyClientOpts -) => { - const client = new SpotifyClient(authProvider, opts) as SpotifyClient & - ISpoitfyAPI; - - for (const name in endpoints) { - client[name as EndpointName] = ( - endpoints[name as EndpointName] as Endpoint - ).bind(null, client); - } - - return client; -}; diff --git a/src/api/artist/artist.endpoints.ts b/src/api/artist/artist.endpoints.ts deleted file mode 100644 index e2574f7..0000000 --- a/src/api/artist/artist.endpoints.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { HTTPClient } from "../client"; -import { PagingObject, PagingOptions } from "../general.types"; -import { AlbumGroup, AlbumSimplified } from "../album/album.types"; -import { Market } from "../market/market.types"; -import { Track } from "../track/track.types"; -import { Artist } from "../artist/artist.types"; -import { SearchParams } from "../../shared"; - -/** - * Get Spotify catalog information for a single artist identified by their unique Spotify ID. - * - * @param client Spotify HTTPClient - * @param artist_id Spotify artist ID - */ -export const getArtist = async (client: HTTPClient, artist_id: string) => { - return await client.fetch("/artists/" + artist_id, "json"); -}; - -/** - * Get Spotify catalog information for several artists based on their Spotify IDs. - * - * @param client Spotify HTTPClient - * @param artist_ids List of the Spotify IDs for the artists. Maximum: 50 IDs. - */ -export const getArtists = async (client: HTTPClient, artist_ids: string[]) => { - return ( - await client.fetch<{ artists: Artist[] }>("/artists", "json", { - query: { - ids: artist_ids - } - }) - ).artists; -}; - -export interface GetArtistAlbumsOpts extends PagingOptions, SearchParams { - /** - * List of keywords that will be used to filter the response. - * If not supplied, all album types will be returned. - */ - include_groups: AlbumGroup[]; - /** - * An ISO 3166-1 alpha-2 country code. - * If a country code is specified, only content that is available in that market will be returned. - */ - market?: Market; -} - -/** - * Get Spotify catalog information about an artist's albums. - * - * @param client Spotify HTTPClient - * @param artist_id Spotify artist ID - */ -export const getArtistAlbums = async ( - client: HTTPClient, - artist_id: string, - opts?: GetArtistAlbumsOpts -) => { - return await client.fetch>( - `/artists/${artist_id}/albums`, - "json", - { query: opts } - ); -}; - -/** - * Get Spotify catalog information about an artist's top tracks by country. - * - * @param client Spotify HTTPClient - * @param artist_id Spotify artist ID - * @param market An ISO 3166-1 alpha-2 country code. - */ -export const getArtistTopTracks = async ( - client: HTTPClient, - artist_id: string, - market: Market // TODO check if it must be required -) => { - return ( - await client.fetch<{ tracks: Track[] }>( - `/artists/${artist_id}/top-tracks`, - "json", - { - query: { - market - } - } - ) - ).tracks; -}; - -/** - * Get Spotify catalog information about artists similar to a given artist. - * Similarity is based on analysis of the Spotify community's listening history. - * - * @param client Spotify HTTPClient - * @param artist_id Spotify artist ID - */ -export const getArtistRelatedArtists = async ( - client: HTTPClient, - artist_id: string -) => { - return ( - await client.fetch<{ artists: Artist[] }>( - `/artists/${artist_id}/related-artists`, - "json" - ) - ).artists; -}; diff --git a/src/api/artist/artist.types.ts b/src/api/artist/artist.types.ts deleted file mode 100644 index 55d6b10..0000000 --- a/src/api/artist/artist.types.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { JSONObject } from "../../shared"; -import { ExternalUrls, Followers, Image } from "../general.types"; - -export interface ArtistSimplified extends JSONObject { - /** - * Known external URLs for this artist. - */ - external_urls: ExternalUrls; - /** - * A link to the Web API endpoint providing full details of the artist. - */ - href: string; - /** - * The Spotify ID for the artist. - */ - id: string; - /** - * The name of the artist. - */ - name: string; - /** - * The object type. - */ - type: "artist"; - /** - * The Spotify URI for the artist. - */ - uri: string; -} - -export interface Artist extends ArtistSimplified, JSONObject { - /** - * Information about the followers of the artist. - */ - followers: Followers; - /** - * A list of the genres the artist is associated with. - * If not yet classified, the array is empty. - */ - genres: string[]; - /** - * Images of the artist in various sizes, widest first. - */ - images: Image[]; - /** - * The popularity of the artist. - * The value will be between 0 and 100, with 100 being the most popular. - * The artist's popularity is calculated from the popularity of all the artist's tracks. - */ - popularity: number; -} diff --git a/src/api/artist/index.ts b/src/api/artist/index.ts deleted file mode 100644 index 38d7d9a..0000000 --- a/src/api/artist/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./artist.endpoints"; -export * from "./artist.types"; diff --git a/src/api/audiobook/audiobook.endpoints.ts b/src/api/audiobook/audiobook.endpoints.ts deleted file mode 100644 index 800d0a8..0000000 --- a/src/api/audiobook/audiobook.endpoints.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { ChapterSimplified } from "../chapter/chapter.types"; -import { HTTPClient } from "../client"; -import { PagingObject, PagingOptions } from "../general.types"; -import { Market } from "../market/market.types"; -import { Audiobook, AudiobookSimplified } from "./audiobook.types"; - -/** - * Get Spotify catalog information for a single Audiobook. - * - * @param client Spotify HTTPClient - * @param audiobook_id The Spotify ID of the Audiobook - * @param market An ISO 3166-1 alpha-2 country code - */ -export const getAudiobook = async ( - client: HTTPClient, - audiobook_id: string, - market?: Market -) => { - return await client.fetch("/audiobooks/" + audiobook_id, "json", { - query: { market } - }); -}; - -/** - * Get Spotify catalog information for multiple audiobooks identified by their Spotify IDs. - * - * @param client Spotify HTTPClient - * @param audiobook_ids List of the Spotify IDs for the audiobooks. Maximum: 20 IDs - * @param market An ISO 3166-1 alpha-2 country code - */ -export const getAudiobooks = async ( - client: HTTPClient, - audiobook_ids: string[], - market?: Market -) => { - return ( - await client.fetch<{ audiobooks: Audiobook[] }>("/audiobooks", "json", { - query: { market, ids: audiobook_ids } - }) - ).audiobooks; -}; - -export interface GetAudiobookChapterOpts extends PagingOptions { - /** - * An ISO 3166-1 alpha-2 country code. - * If a country code is specified, only content that is available in that market will be returned. - */ - market?: Market; -} - -/** - * Get Spotify catalog information about an audiobook’s Chapters. - * Optional parameters can be used to limit the number of Chapters returned. - * - * @param client Spotify HTTPClient - * @param audiobook_id The Spotify ID of the audiobook - * @param opts Additional option for request - */ -export const getAudiobookChapters = async ( - client: HTTPClient, - audiobook_id: string, - opts?: GetAudiobookChapterOpts -) => { - return await client.fetch>( - `/audiobooks/${audiobook_id}/chapters`, - "json", - { - query: opts - } - ); -}; - -export interface GetSavedAudiobooksOpts extends PagingOptions { - /** - * An ISO 3166-1 alpha-2 country code. - * If a country code is specified, only content that is available in that market will be returned. - */ - market?: Market; -} - -/** - * Get a list of the audiobooks saved in the current Spotify user's 'Your Audiobooks' library. - * - * @param client Spotify HTTPClient - * @param opts Additional option for request - */ -export const getSavedAudiobooks = async ( - client: HTTPClient, - opts?: GetSavedAudiobooksOpts -) => { - return await client.fetch< - PagingObject<{ - /** - * The date and time the audiobook was saved Timestamps are returned in ISO 8601 format as Coordinated Universal Time (UTC) with a zero offset: YYYY-MM-DDTHH:MM:SSZ. - */ - added_at: string; - /** - * Information about the audiobook. - */ - audiobook: AudiobookSimplified; - }> - >("/me/audiobooks", "json", { - query: opts - }); -}; - -/** - * Save one or more audiobooks to the current user's 'Your Audiobooks' library. - * - * @param client Spotify HTTPClient - * @param audiobooks_ids List of the Spotify IDs for the audiobooks. Maximum: 20 IDs - */ -export const saveAudiobooks = async ( - client: HTTPClient, - audiobooks_ids: string[] -) => { - await client.fetch("/me/audiobooks", "void", { - method: "PUT", - query: { ids: audiobooks_ids } - }); -}; - -/** - * Save audiobook to the current user's 'Your Audiobooks' library. - * - * @param client Spotify HTTPClient - * @param audiobooks_id The Spotify ID of the audiobook - */ -export const saveAudiobook = async ( - client: HTTPClient, - audiobook_id: string -) => { - await saveAudiobooks(client, [audiobook_id]); -}; - -/** - * Remove one or more audiobooks from the current user's 'Your Audiobooks' library. - * - * @param client Spotify HTTPClient - * @param audiobook_ids List of the Spotify IDs for the audiobooks. Maximum: 20 IDs - */ -export const removeSavedAudiobooks = async ( - client: HTTPClient, - audiobook_ids: string[] -) => { - await client.fetch("/me/audiobooks", "void", { - method: "DELETE", - query: { - ids: audiobook_ids - } - }); -}; - -/** - * Remove audiobook from the current user's 'Your Audiobooks' library. - * - * @param client Spotify HTTPClient - * @param audiobook_id The Spotify ID of the audiobook - */ -export const removeSavedAudiobook = async ( - client: HTTPClient, - audiobook_id: string -) => { - await removeSavedAudiobooks(client, [audiobook_id]); -}; - -/** - * Check if one or more audiobooks is already saved in the current Spotify user's 'Your Audiobooks' library. - * - * @param client Spotify HTTPClient - * @param audiobook_ids List of the Spotify IDs for the audiobooks. Maximum: 20 IDs - */ -export const checkSavedAudiobooks = async ( - client: HTTPClient, - audiobook_ids: string[] -) => { - return await client.fetch("/me/audiobooks/contains", "json", { - query: { - ids: audiobook_ids - } - }); -}; - -/** - * Check if audiobook is already saved in the current Spotify user's 'Your Audiobooks' library. - * - * @param client Spotify HTTPClient - * @param audiobook_id The Spotify ID of the Audiobook - */ -export const checkSavedAudiobook = async ( - client: HTTPClient, - audiobook_id: string -) => { - return (await checkSavedAudiobooks(client, [audiobook_id]))[0]; -}; diff --git a/src/api/audiobook/audiobook.types.ts b/src/api/audiobook/audiobook.types.ts deleted file mode 100644 index 881dc28..0000000 --- a/src/api/audiobook/audiobook.types.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { JSONObject } from "../../shared"; -import { ChapterSimplified } from "../chapter/chapter.types"; -import { - Author, - Copyright, - ExternalUrls, - Narrator, - Image, - PagingObject -} from "../general.types"; -import { Market } from "../market/market.types"; - -export interface AudiobookSimplified extends JSONObject { - /** - * The author(s) for the audiobook. - */ - authors: Author[]; - /** - * A list of the countries in which the audiobook can be played. - */ - available_markets: Market[]; - /** - * The copyright statements of the audiobook. - */ - copyrights: Copyright[]; - /** - * The description of the audiobook without html tags. - */ - description: string; - /** - * The description of the audiobook with html tags. - */ - html_description: string; - /** - * The edition of the audiobook. - */ - edition: string; - /** - * Whether or not the audiobook has explicit lyrics. - */ - explicit: boolean; - /** - * External URLs for this audiobook. - */ - external_urls: ExternalUrls; - /** - * A link to the Web API endpoint providing full details of the audiobook. - */ - href: string; - /** - * The Spotify ID for the audiobook. - */ - id: string; - /** - * Images of the audiobook in various sizes, widest first. - */ - images: Image[]; - /** - * A list of the languages used in the audiobook, identified by their ISO 639-1 code. - */ - languages: string[]; - /** - * The media type of the audiobook. - */ - media_type: string; - /** - * The name of the audiobook. - */ - name: string; - /** - * The narrator(s) for the audiobook. - */ - narrators: Narrator[]; - /** - * The publisher of the audiobook. - */ - publisher: string; - /** - * The object type. - */ - type: "audiobook"; - /** - * The Spotify URI for the audiobook. - */ - uri: string; - - /** - * The number of chapters in this audiobook. - */ - total_chapters: number; -} - -export interface Audiobook extends AudiobookSimplified, JSONObject { - /** - * The chapters of the audiobook. - */ - chapters: PagingObject; -} diff --git a/src/api/audiobook/index.ts b/src/api/audiobook/index.ts deleted file mode 100644 index 8940c4d..0000000 --- a/src/api/audiobook/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./audiobook.endpoints"; -export * from "./audiobook.types"; diff --git a/src/api/category/category.endpoints.ts b/src/api/category/category.endpoints.ts deleted file mode 100644 index 50f5e30..0000000 --- a/src/api/category/category.endpoints.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { SearchParams } from "../../shared"; -import { PagingObject } from "../general.types"; -import { Market } from "../market/market.types"; -import { Category } from "../category/category.types"; -import { HTTPClient } from "../client"; - -export interface GetBrowseCategoriesOpts extends SearchParams { - /** - * An ISO 3166-1 alpha-2 country code. - * If a country code is specified, only content that is available in that market will be returned. - */ - country?: Market; - /** - * The maximum number of items to return. Minimum: 1. Maximum: 50. - * @default 20 - */ - limit?: number; - /** - * The desired language, consisting of an ISO 639-1 language code and an ISO 3166-1 alpha-2 country code, joined by an underscore. - * - * Provide this parameter if you want the category metadata returned in a particular language. - * - * @example "es_MX" - meaning "Spanish (Mexico)". - */ - locale?: string; - /** - * The index of the first item to return. - * Use with limit to get the next set of items. - * - * @default 0 - */ - offset?: number; -} - -/** - * Get a list of categories used to tag items in Spotify - * (on, for example, the Spotify player’s “Browse” tab). - * - * @param client Spotify HTTPClient - * @param opts Additional option for request - */ -export const getBrowseCategories = async ( - client: HTTPClient, - opts?: GetBrowseCategoriesOpts -) => { - return ( - await client.fetch<{ categories: PagingObject }>( - "/browse/categories", - "json", - { - query: opts - } - ) - ).categories; -}; - -export interface GetBrowseCategoryOpts extends SearchParams { - /** - * An ISO 3166-1 alpha-2 country code. - * Provide this parameter to ensure that the category exists for a particular country. - */ - country?: Market; - /** - * The maximum number of items to return. Minimum: 1. Maximum: 50. - * @default 20 - */ - limit?: number; -} - -/** - * Get a single category used to tag items in Spotify - * (on, for example, the Spotify player’s “Browse” tab). - * - * @param client Spotify HTTPClient - * @param category_id The Spotify category ID for the category - * @param opts Additional option for request - */ -export const getBrowseCategory = async ( - client: HTTPClient, - category_id: string, - opts?: GetBrowseCategoryOpts -) => { - return await client.fetch( - "/browse/categories/" + category_id, - "json", - { - query: opts - } - ); -}; diff --git a/src/api/category/category.types.ts b/src/api/category/category.types.ts deleted file mode 100644 index 5c0ca20..0000000 --- a/src/api/category/category.types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { JSONObject, NonNullableJSON } from "../../shared"; -import { Image } from "../general.types"; - -export interface Category extends JSONObject { - /** - * A link to the Web API endpoint returning full details of the category. - */ - href: string; - /** - * The category icon, in various sizes. - */ - icons: NonNullableJSON[]; - /** - * The Spotify category ID of the category. - */ - id: string; - /** - * The name of the category. - */ - name: string; -} diff --git a/src/api/category/index.ts b/src/api/category/index.ts deleted file mode 100644 index 1688b09..0000000 --- a/src/api/category/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./category.endpoints"; -export * from "./category.types"; diff --git a/src/api/chapter/chapter.endpoints.ts b/src/api/chapter/chapter.endpoints.ts deleted file mode 100644 index d0a303f..0000000 --- a/src/api/chapter/chapter.endpoints.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { HTTPClient } from "../client"; -import { Market } from "../market/market.types"; -import { Chapter } from "./chapter.types"; - -/** - * - * @param client Spotify HTTPClient - * @param chapter_id The Spotify ID of the chapter - * @param market An ISO 3166-1 alpha-2 country code - */ -export const getChapter = async ( - client: HTTPClient, - chapter_id: string, - market?: Market -) => { - return await client.fetch("/chapters/" + chapter_id, "json", { - query: { market } - }); -}; - -/** - * - * @param client Spotify HTTPClient - * @param chapter_ids List of the Spotify IDs of the chapters. Maximum: 20 IDs - * @param market An ISO 3166-1 alpha-2 country code - * @returns - */ -export const getChapters = async ( - client: HTTPClient, - chapter_ids: string[], - market?: Market -) => { - return ( - await client.fetch<{ chapters: Chapter[] }>("/chapters", "json", { - query: { market, ids: chapter_ids } - }) - ).chapters; -}; diff --git a/src/api/chapter/chapter.types.ts b/src/api/chapter/chapter.types.ts deleted file mode 100644 index f010e8e..0000000 --- a/src/api/chapter/chapter.types.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { JSONObject } from "../../shared"; -import { AudiobookSimplified } from "../audiobook/audiobook.types"; -import { - ExternalUrls, - Image, - RestrictionsReason, - ResumePoint -} from "../general.types"; -import { Market } from "../market/market.types"; - -export interface ChapterSimplified extends JSONObject { - /** - * A URL to a 30 second preview (MP3 format). - */ - audio_preview_url: string; - /** - * A list of the countries in which the episode can be played. - */ - available_markets: Market[]; - /** - * The number of the episode - */ - chapter_number: number; - /** - * The description of the episode without html tags. - */ - description: string; - /** - * The description of the episode with html tags. - */ - html_description: string; - /** - * The episode length in milliseconds. - */ - duration_ms: number; - /** - * Whether or not the episode has explicit lyrics. - */ - explicit: boolean; - /** - * External URLs for this episode. - */ - external_urls: ExternalUrls; - /** - * A link to the Web API endpoint providing full details of the episode. - */ - href: string; - /** - * The Spotify ID for the episode. - */ - id: string; - /** - * Images of the episode in various sizes, widest first. - */ - images: Image[]; - /** - * If true, the episode is playable in the given market. - * Otherwise false. - */ - is_playable: boolean; - /** - * A list of the languages used in the episode, identified by their ISO 639-1 code. - */ - languages: string[]; - /** - * The name of the episode. - */ - name: string; - /** - * The date the episode was first released. - * Depending on the precision it might be shown in different ways - */ - release_date: string; - /** - * The precision with which `release_date` value is known. - */ - release_date_precision: "year" | "month" | "day"; - /** - * The user's most recent position in the episode. - */ - resume_point: ResumePoint; - /** - * The object type. - */ - type: "episode"; - /** - * The Spotify URI for the episode. - */ - uri: string; - /** - * Included in the response when a content restriction is applied. - */ - restrictions?: { - /** - * The reason for the restriction. - * - * Episodes may be restricted if the content is not available in a given market, to the user's subscription type, or when the user's account is set to not play explicit content. - */ - reason: RestrictionsReason; - }; -} - -export interface Chapter extends ChapterSimplified, JSONObject { - audiobook: AudiobookSimplified; -} diff --git a/src/api/chapter/index.ts b/src/api/chapter/index.ts deleted file mode 100644 index ba4e0ce..0000000 --- a/src/api/chapter/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./chapter.endpoints"; -export * from "./chapter.types"; diff --git a/src/api/client.ts b/src/api/client.ts deleted file mode 100644 index 16a2f63..0000000 --- a/src/api/client.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { - IAuthProvider, - JSONValue, - parseResponse, - SearchParams, - toQueryString -} from "../shared"; - -/** - * @see https://developer.spotify.com/documentation/web-api/concepts/api-calls#regular-error-object - */ -type RegularErrorObject = { - error: { - message: string; - status: number; - reason?: string; - }; -}; - -export class APIError extends Error { - constructor( - public readonly raw: string | RegularErrorObject, - public readonly status: number, - options?: ErrorOptions - ) { - super(typeof raw === "object" ? raw.error.message : raw, options); - this.name = "APIError"; - } -} - -export class RateLimitError extends APIError { - constructor( - raw: string | RegularErrorObject, - public readonly retryAfter: number, - options?: ErrorOptions - ) { - super(raw, 429, options); - this.name = "RateLimitError"; - } -} - -/** - * Options for fetch method on HTTPClient interface - */ -export interface FetchOpts { - method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; - /** - * Json object that will be stringified and passed to the body of the request. - * Will be ignored if `body` is specified. - */ - json?: JSONValue; - /** - * Query parameters or, more precisely, search parameters that will be converted into a query string and passed to the url. - */ - query?: SearchParams; - /** - * Custom headers that will be passed to the request and overwrite existing ones. - */ - headers?: Record; - /** - * Request body. Will overwrite `json` property if specified. - */ - body?: BodyInit; -} - -export type ExpectedResponse = "json" | "void"; - -/** - * Interface that provides a fetch method to make HTTP requests to Spotify API. - */ -export interface HTTPClient { - /** - * Sends an HTTP request. - * - * @param baseURL - * The base URL for the API request. Must begin with "/" - * @param returnType - * The expected return type of the API response. - * @param opts - * Optional request options, such as the request body or query parameters. - */ - fetch(baseURL: string, responseType: "void", opts?: FetchOpts): Promise; - fetch( - baseURL: string, - responseType: "json", - opts?: FetchOpts - ): Promise; -} - -export interface SpotifyClientOpts { - /** - * How many times do you want to try again until you throw the error - * - * @default 0 - */ - retryTimesOn5xx?: number; - /** - * How long in miliseconds do you want to wait until the next retry - * - * @default 0 - */ - retryDelayOn5xx?: number; - /** - * If it is set to true, it would refetch the same request after a paticular time interval sent by the spotify api in the `Retry-After` header, so you cannot face any obstacles. - * Otherwise, it will throw an `RateLimitError`. - * - * @default false - */ - waitForRateLimit?: boolean; - onUnauthorized?: (client: SpotifyClient) => void; -} - -/** - * A client for making requests to the Spotify API. - */ -export class SpotifyClient< - T extends IAuthProvider | string = IAuthProvider | string -> implements HTTPClient -{ - private static readonly BASE_HEADERS: HeadersInit = { - "Content-Type": "application/json", - Accept: "application/json" - }; - - private authProvider!: T; - // Bearer access token - private token: string | undefined; - - /** - * @param authProvider It is recommended to pass an object that implements `IAuthProvider` to automatically update tokens. If you do not need this behavior,you can simply pass an access token. - * @param opts Additional options for fetch execution, such as the ability to retry on error - */ - constructor( - authProvider: T, - private readonly opts: SpotifyClientOpts = { - waitForRateLimit: false, - retryDelayOn5xx: 0, - retryTimesOn5xx: 0 - } - ) { - this.setAuthProvider(authProvider); - } - - /** - * Method that changes the existing authProvider to the specified one - */ - setAuthProvider(authProvider: T) { - this.authProvider = authProvider; - this.token = - typeof authProvider === "string" ? authProvider : authProvider.token; - } - - fetch(baseURL: string, hasResponse: "void", opts?: FetchOpts): Promise; - fetch( - baseURL: string, - responseType: "json", - opts?: FetchOpts - ): Promise; - async fetch( - baseURL: string, - responseType: ExpectedResponse, - { json, query, method, headers, body }: FetchOpts = {} - ): Promise { - const url = new URL("https://api.spotify.com/v1" + baseURL); - if (query) url.search = toQueryString(query); - - const _body = body ? body : json ? JSON.stringify(json) : undefined; - const _headers = new Headers(SpotifyClient.BASE_HEADERS); - if (headers) { - for (const key in headers) { - _headers.append(key, headers[key]); - } - } - - let isRefreshed = false; - let retries5xx = this.opts.retryTimesOn5xx; - - if (typeof this.authProvider === "object" && !this.token) { - try { - this.token = await this.authProvider.refresh(); - } catch (error) { - if (this.opts.onUnauthorized) this.opts.onUnauthorized(this); - throw error; - } - isRefreshed = true; - } - - // recursive function - const call = async (): Promise => { - _headers.set("Authorization", "Bearer " + this.token); - - const res = await fetch(url, { - method, - headers: _headers, - body: _body - }); - - if (res.ok) return res; - - const rawError = await parseResponse(res); - - if ( - res.status === 401 && - typeof this.authProvider === "object" && - !isRefreshed - ) { - try { - this.token = await this.authProvider.refresh(); - } catch (error) { - if (this.opts.onUnauthorized) this.opts.onUnauthorized(this); - throw error; - } - - isRefreshed = true; - return call(); - } - - if (res.status === 429) { - // time in seconds - const retryAfter = Number(res.headers.get("Retry-After")) || 0; - - if (this.opts.waitForRateLimit) { - await new Promise((r) => setTimeout(r, retryAfter * 1000)); - return call(); - } - - throw new RateLimitError(rawError, retryAfter); - } - - if (res.status >= 500 && retries5xx) { - if (this.opts.retryDelayOn5xx) { - await new Promise((r) => setTimeout(r, this.opts.retryDelayOn5xx)); - } - - retries5xx--; - return call(); - } - - if (res.status === 401 && this.opts.onUnauthorized) { - this.opts.onUnauthorized(this); - } - - throw new APIError(rawError, res.status); - }; - - const res = await call(); - - if (responseType === "json") { - if (!res.body) throw new Error("Body not found"); - return (await res.json()) as R; - } - if (res.body) res.body.cancel(); - } -} diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts deleted file mode 100644 index 612b7fe..0000000 --- a/src/api/endpoints.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from "./album"; -export * from "./artist"; -export * from "./category"; -export * from "./genre"; -export * from "./market"; -export * from "./playlist"; -export * from "./track"; -export * from "./user"; -export * from "./episode"; -export * from "./show"; -export * from "./chapter"; -export * from "./audiobook"; -export * from "./search"; diff --git a/src/api/episode/episode.endpoints.ts b/src/api/episode/episode.endpoints.ts deleted file mode 100644 index 8dc37fe..0000000 --- a/src/api/episode/episode.endpoints.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { HTTPClient } from "../client"; -import { Market } from "../market/market.types"; -import { Episode } from "../episode/episode.types"; -import { PagingObject, PagingOptions } from "../general.types"; - -/** - * Get Spotify catalog informnation for a single episode. - * - * @param client Spotify HTTPClient - * @param episode_id The Spotify ID of the episode - * @param market An ISO 3166-1 alpha-2 country code - */ -export const getEpisode = async ( - client: HTTPClient, - episode_id: string, - market?: Market -) => { - return await client.fetch("/episodes/" + episode_id, "json", { - query: { market } - }); -}; - -/** - * Get spotify catalog information for multiple episodes identified by their Spotify IDs. - * - * @param client Spotify HTTPClient - * @param episodes_ids List of the Spotify IDs of the episodes. Maximum: 20 IDs - * @param market An ISO 3166-1 alpha-2 country code - */ -export const getEpisodes = async ( - client: HTTPClient, - episodes_ids: string[], - market?: Market -) => { - return ( - await client.fetch<{ episodes: Episode[] }>("/episodes", "json", { - query: { market, ids: episodes_ids } - }) - ).episodes; -}; - -export interface GetSavedEpisodesOpts extends PagingOptions { - /** - * An ISO 3166-1 alpha-2 country code. - * If a country code is specified, only content that is available in that market will be returned. - */ - market?: Market; -} - -/** - * Get a list of the episodes saved in the current Spotify user's 'Your Shows' library. - * - * @param client Spotify HTTPClient - * @param opts Additional option for request - */ -export const getSavedEpisodes = async ( - client: HTTPClient, - opts?: GetSavedEpisodesOpts -) => { - return await client.fetch< - PagingObject<{ - /** - * The date and time the episode was saved Timestamps are returned in ISO 8601 format as Coordinated Universal Time (UTC) with a zero offset: YYYY-MM-DDTHH:MM:SSZ. - */ - added_at: string; - /** - * Information about the episode. - */ - episode: Episode; - }> - >("/me/episodes", "json", { - query: opts - }); -}; - -/** - * Save one or more episodes to the current user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param episodes_ids List of the Spotify IDs for the episodes. Maximum: 20 IDs - */ -export const saveEpisodes = async ( - client: HTTPClient, - episodes_ids: string[] -) => { - await client.fetch("/me/episodes", "void", { - method: "PUT", - query: { ids: episodes_ids } - }); -}; - -/** - * Save episode to the current user's 'Your Shows' library. - * - * @param client Spotify HTTPClient - * @param episode_id The Spotify ID of the episode - */ -export const saveEpisode = async (client: HTTPClient, episode_id: string) => { - await saveEpisodes(client, [episode_id]); -}; - -/** - * Remove one or more episodes from the current user's 'Your Shows' library. - * - * @param client Spotify HTTPClient - * @param episodes_ids List of the Spotify IDs for the episodes. Maximum: 20 IDs - */ -export const removeSavedEpisodes = async ( - client: HTTPClient, - episodes_ids: string[] -) => { - await client.fetch("/me/episodes", "void", { - method: "DELETE", - query: { - ids: episodes_ids - } - }); -}; - -/** - * Remove episode from the current user's 'Your Shows' library. - * - * @param client Spotify HTTPClient - * @param episodes_id List of the Spotify IDs for the episodes. Maximum: 20 IDs - */ -export const removeSavedEpisode = async ( - client: HTTPClient, - episodes_id: string -) => { - await removeSavedEpisodes(client, [episodes_id]); -}; - -/** - * Check if one or more episodes is already saved in the current Spotify user's 'Your Shows' library. - * - * @param client Spotify HTTPClient - * @param episodes_ids List of the Spotify IDs for the episodes. Maximum: 20 IDs - */ -export const checkSavedEpisodes = async ( - client: HTTPClient, - episodes_ids: string[] -) => { - return await client.fetch("/me/albums/contains", "json", { - query: { - ids: episodes_ids - } - }); -}; - -/** - * Check if epsisode is already saved in the current Spotify user's 'Your Shows' library. - * - * @param client Spotify HTTPClient - * @param episode_id The Spotify ID for the episode - */ -export const checkSaveEpisode = async ( - client: HTTPClient, - episode_id: string -) => { - return (await checkSavedEpisodes(client, [episode_id]))[0]; -}; diff --git a/src/api/episode/episode.types.ts b/src/api/episode/episode.types.ts deleted file mode 100644 index 558a91f..0000000 --- a/src/api/episode/episode.types.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { JSONObject } from "../../shared"; -import { - ExternalUrls, - Image, - RestrictionsReason, - ResumePoint -} from "../general.types"; -import { ShowSimplified } from "../show/show.types"; - -export interface EpisodeSimplified extends JSONObject { - /** - * A URL to a 30 second preview (MP3 format). - */ - audio_preview_url: string; - /** - * A description of the episode without HTML tags. - */ - description: string; - /** - * A description of the episode with HTML tags. - */ - html_description: string; - /** - * The episode length in milliseconds. - */ - duration_ms: number; - /** - * Whether or not the episode has explicit lyrics. - */ - explicit: boolean; - /** - * External URLs for this episode. - */ - external_urls: ExternalUrls; - /** - * A link to the Web API endpoint providing full details of the episode. - */ - href: string; - /** - * The Spotify ID for the episode. - */ - id: string; - /** - * Images of the episode in various sizes, widest first. - */ - images: Image[]; - /** - * True, if the episode is hosted outside of Spotify's CDN. - */ - is_externally_hosted: boolean; - /** - * If true, the episode is playable in the given market. - * Otherwise false. - */ - is_playable: boolean; - /** - * The language used in the episode. Identified by a ISO 639 code. Deprecated. - */ - language?: string; - /** - * A list of the languages used in the episode, identified by their ISO 639-1 code. - */ - languages: string[]; - /** - * The name of the episode. - */ - name: string; - /** - * The date the episode was first released. - * Depending on the precision it might be shown in different ways - */ - release_date: string; - /** - * The precision with which `release_date` value is known. - */ - release_date_precision: "year" | "month" | "day"; - /** - * The user's most recent position in the episode. - */ - resume_point: ResumePoint; - /** - * The object type. - */ - type: "episode"; - /** - * The Spotify URI for the episode. - */ - uri: string; - /** - * Included in the response when a content restriction is applied. - */ - restrictions?: { - /** - * The reason for the restriction. - * - * Episodes may be restricted if the content is not available in a given market, to the user's subscription type, or when the user's account is set to not play explicit content. - */ - reason: RestrictionsReason; - }; -} - -export interface Episode extends EpisodeSimplified, JSONObject { - /** - * The show on which episode belongs. - */ - show: ShowSimplified; -} diff --git a/src/api/episode/index.ts b/src/api/episode/index.ts deleted file mode 100644 index 0e9c6f5..0000000 --- a/src/api/episode/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./episode.endpoints"; -export * from "./episode.types"; diff --git a/src/api/general.types.ts b/src/api/general.types.ts deleted file mode 100644 index 96782e4..0000000 --- a/src/api/general.types.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { JSONObject, SearchParams } from "../shared"; - -export interface PagingObject extends JSONObject { - /** - * A link to the Web API endpoint returning the full result of the request. - */ - href: string; - /** - * The maximum number of items in the response. - */ - limit: number; - /** - * URL to the next page of items. - */ - next: string | null; - /** - * The offset of the items returned. - */ - offset: number; - /** - * URL to the previous page of items - */ - previous: string | null; - /** - * The total number of items available to return. - */ - total: number; - items: T[]; -} - -export interface CursorPagingObject extends JSONObject { - /** - * A link to the Web API endpoint returning the full result of the request. - */ - href: string; - /** - * The maximum number of items in the response. - */ - limit: number; - /** - * URL to the next page of items. - */ - next: string | null; - /** - * The cursors used to find the next set of items. - */ - cursors: { - /** - * The cursor to use as key to find the next page of items. - */ - after: string; - /** - * The cursor to use as key to find the previous page of items. - */ - before: string; - }; - /** - * The total number of items available to return. - */ - total: number; - items: T[]; -} - -export interface PagingOptions extends SearchParams { - /** - * The maximum number of items to return. Minimum: 1. Maximum: 50. - * @default 20 - */ - limit?: number; - /** - * The index of the first item to return. Use with limit to get the next set of items. - * @default 0 (the first item) - */ - offset?: number; -} - -/** - * The reason for the restriction. - * - * "market" - The content item is not available in the given market. \ - * "product" - The content item is not available for the user's subscription type. \ - * "explicit" - The content item is explicit and the user's account is set to not play explicit content. - */ -export type RestrictionsReason = "market" | "product" | "explicit"; - -export interface Image extends JSONObject { - /** - * The image height in pixels. - */ - height: number | null; - /** - * The source URL of the image. - */ - url: string; - /** - * The image width in pixels. - */ - width: number | null; -} - -export interface ResumePoint extends JSONObject { - /** - * Whether or not the episode has been fully played by the user. - */ - fully_played: boolean; - /** - * The user's most recent position in the episode in milliseconds - */ - resume_position_ms: number; -} - -export interface Followers extends JSONObject { - /** - * This will always be set to null, as the Web API does not support it at the moment. - */ - href: string | null; - /** - * The total number of followers. - */ - total: number; -} - -export interface Author extends JSONObject { - /** - * The name of the author. - */ - name: string; -} - -export interface Narrator extends JSONObject { - /** - * The name of the narrator. - */ - name: string; -} - -export interface ExternalUrls extends JSONObject { - spotify: string; -} - -export interface ExternalIds extends JSONObject { - /** - * [International Standard Recording Code](https://en.wikipedia.org/wiki/International_Standard_Recording_Code). - */ - isrc?: string; - /** - * [International Article Number](http://en.wikipedia.org/wiki/International_Article_Number_%28EAN%29). - */ - ean?: string; - /** - * [Universal Product Code](http://en.wikipedia.org/wiki/Universal_Product_Code). - */ - upc?: string; -} - -/** - * The copyright object contains the type and the name of copyright. - */ -export interface Copyright extends JSONObject { - /** - * The copyright text for this content. - */ - text: string; - /** - * The type of copyright: \ - * C = the copyright \ - * P = the sound recording (performance) copyright - */ - type: "C" | "P"; -} diff --git a/src/api/genre/genre.endpoints.ts b/src/api/genre/genre.endpoints.ts deleted file mode 100644 index 2458a11..0000000 --- a/src/api/genre/genre.endpoints.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { HTTPClient } from "../client"; -import { Genre } from "../genre/genre.types"; - -/** - * Retrieve a list of available genres seed parameter values for recommendations. - * - * @param client Spotify HTTPClient - */ -export const getAvailableGenres = async (client: HTTPClient) => { - return ( - await client.fetch<{ genres: Genre[] }>( - "/recommendations/available-genre-seeds", - "json" - ) - ).genres; -}; diff --git a/src/api/genre/genre.types.ts b/src/api/genre/genre.types.ts deleted file mode 100644 index 9c77ed2..0000000 --- a/src/api/genre/genre.types.ts +++ /dev/null @@ -1,127 +0,0 @@ -export type Genre = - | "acoustic" - | "afrobeat" - | "alt-rock" - | "alternative" - | "ambient" - | "anime" - | "black-metal" - | "bluegrass" - | "blues" - | "bossanova" - | "brazil" - | "breakbeat" - | "british" - | "cantopop" - | "chicago-house" - | "children" - | "chill" - | "classical" - | "club" - | "comedy" - | "country" - | "dance" - | "dancehall" - | "death-metal" - | "deep-house" - | "detroit-techno" - | "disco" - | "disney" - | "drum-and-bass" - | "dub" - | "dubstep" - | "edm" - | "electro" - | "electronic" - | "emo" - | "folk" - | "forro" - | "french" - | "funk" - | "garage" - | "german" - | "gospel" - | "goth" - | "grindcore" - | "groove" - | "grunge" - | "guitar" - | "happy" - | "hard-rock" - | "hardcore" - | "hardstyle" - | "heavy-metal" - | "hip-hop" - | "holidays" - | "honky-tonk" - | "house" - | "idm" - | "indian" - | "indie" - | "indie-pop" - | "industrial" - | "iranian" - | "j-dance" - | "j-idol" - | "j-pop" - | "j-rock" - | "jazz" - | "k-pop" - | "kids" - | "latin" - | "latino" - | "malay" - | "mandopop" - | "metal" - | "metal-misc" - | "metalcore" - | "minimal-techno" - | "movies" - | "mpb" - | "new-age" - | "new-release" - | "opera" - | "pagode" - | "party" - | "philippines-opm" - | "piano" - | "pop" - | "pop-film" - | "post-dubstep" - | "power-pop" - | "progressive-house" - | "psych-rock" - | "punk" - | "punk-rock" - | "r-n-b" - | "rainy-day" - | "reggae" - | "reggaeton" - | "road-trip" - | "rock" - | "rock-n-roll" - | "rockabilly" - | "romance" - | "sad" - | "salsa" - | "samba" - | "sertanejo" - | "show-tunes" - | "singer-songwriter" - | "ska" - | "sleep" - | "songwriter" - | "soul" - | "soundtracks" - | "spanish" - | "study" - | "summer" - | "swedish" - | "synth-pop" - | "tango" - | "techno" - | "trance" - | "trip-hop" - | "turkish" - | "work-out" - | "world-music"; diff --git a/src/api/genre/index.ts b/src/api/genre/index.ts deleted file mode 100644 index bbd35ed..0000000 --- a/src/api/genre/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./genre.endpoints"; -export * from "./genre.types"; diff --git a/src/api/index.ts b/src/api/index.ts deleted file mode 100644 index 1a071d9..0000000 --- a/src/api/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from "./endpoints"; -export * from "./general.types"; -export * from "./api"; -export * from "./client"; - -export { - type IAuthProvider, - type SearchParam, - type SearchParams -} from "../shared"; diff --git a/src/api/market/index.ts b/src/api/market/index.ts deleted file mode 100644 index 0af7505..0000000 --- a/src/api/market/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./market.endpoints"; -export * from "./market.types"; diff --git a/src/api/market/market.endpoints.ts b/src/api/market/market.endpoints.ts deleted file mode 100644 index d4b6b5e..0000000 --- a/src/api/market/market.endpoints.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { HTTPClient } from "../client"; -import { Market } from "../market/market.types"; - -/** - * Get the list of markets where Spotify is available. - * - * @param client Spotify HTTPClient - */ -export const getAvailableMarkets = async (client: HTTPClient) => { - return (await client.fetch<{ markets: Market[] }>("/markets", "json")) - .markets; -}; diff --git a/src/api/market/market.types.ts b/src/api/market/market.types.ts deleted file mode 100644 index 8318cbd..0000000 --- a/src/api/market/market.types.ts +++ /dev/null @@ -1,186 +0,0 @@ -export type Market = - | "AD" - | "AE" - | "AG" - | "AL" - | "AM" - | "AO" - | "AR" - | "AT" - | "AU" - | "AZ" - | "BA" - | "BB" - | "BD" - | "BE" - | "BF" - | "BG" - | "BH" - | "BI" - | "BJ" - | "BN" - | "BO" - | "BR" - | "BS" - | "BT" - | "BW" - | "BY" - | "BZ" - | "CA" - | "CD" - | "CG" - | "CH" - | "CI" - | "CL" - | "CM" - | "CO" - | "CR" - | "CV" - | "CW" - | "CY" - | "CZ" - | "DE" - | "DJ" - | "DK" - | "DM" - | "DO" - | "DZ" - | "EC" - | "EE" - | "EG" - | "ES" - | "FI" - | "FJ" - | "FM" - | "FR" - | "GA" - | "GB" - | "GD" - | "GE" - | "GH" - | "GM" - | "GN" - | "GQ" - | "GR" - | "GT" - | "GW" - | "GY" - | "HK" - | "HN" - | "HR" - | "HT" - | "HU" - | "ID" - | "IE" - | "IL" - | "IN" - | "IQ" - | "IS" - | "IT" - | "JM" - | "JO" - | "JP" - | "KE" - | "KG" - | "KH" - | "KI" - | "KM" - | "KN" - | "KR" - | "KW" - | "KZ" - | "LA" - | "LB" - | "LC" - | "LI" - | "LK" - | "LR" - | "LS" - | "LT" - | "LU" - | "LV" - | "LY" - | "MA" - | "MC" - | "MD" - | "ME" - | "MG" - | "MH" - | "MK" - | "ML" - | "MN" - | "MO" - | "MR" - | "MT" - | "MU" - | "MV" - | "MW" - | "MX" - | "MY" - | "MZ" - | "NA" - | "NE" - | "NG" - | "NI" - | "NL" - | "NO" - | "NP" - | "NR" - | "NZ" - | "OM" - | "PA" - | "PE" - | "PG" - | "PH" - | "PK" - | "PL" - | "PS" - | "PT" - | "PW" - | "PY" - | "QA" - | "RO" - | "RS" - | "RU" - | "RW" - | "SA" - | "SB" - | "SC" - | "SE" - | "SG" - | "SI" - | "SK" - | "SL" - | "SM" - | "SN" - | "SR" - | "ST" - | "SV" - | "SZ" - | "TD" - | "TG" - | "TH" - | "TJ" - | "TL" - | "TN" - | "TO" - | "TR" - | "TT" - | "TV" - | "TW" - | "TZ" - | "UA" - | "UG" - | "US" - | "UY" - | "UZ" - | "VC" - | "VE" - | "VN" - | "VU" - | "WS" - | "XK" - | "ZA" - | "ZM" - | "ZW" - | "from_token"; diff --git a/src/api/playlist/index.ts b/src/api/playlist/index.ts deleted file mode 100644 index 588ca6a..0000000 --- a/src/api/playlist/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./playlist.endpoints"; -export * from "./playlist.types"; diff --git a/src/api/playlist/playlist.endpoints.ts b/src/api/playlist/playlist.endpoints.ts deleted file mode 100644 index 52d03ac..0000000 --- a/src/api/playlist/playlist.endpoints.ts +++ /dev/null @@ -1,451 +0,0 @@ -import { SearchParams, JSONObject, NonNullableJSON } from "../../shared"; -import { Market } from "../market/market.types"; -import { Image, PagingObject, PagingOptions } from "../general.types"; -import { - FeaturedPlaylists, - Playlist, - PlaylistSimplified, - PlaylistTrack, - SnapshotResponse -} from "../playlist/playlist.types"; -import { HTTPClient } from "../client"; - -export interface PlaylistFieldsOpts extends SearchParams { - /** - * List of item types that your client supports besides the default track type. - */ - additional_types?: ("track" | "episode")[]; - /** - * Filters for the query: a comma-separated list of the fields to return. - * If omitted, all fields are returned. - */ - fields?: string; -} - -export interface GetPlaylistOpts extends SearchParams, PlaylistFieldsOpts { - /** - * An ISO 3166-1 alpha-2 country code. - * If a country code is specified, only content that is available in that market will be returned. - */ - market?: Market; -} - -/** - * Get a playlist owned by a Spotify user - * - * @param client Spotify HTTPClient - * @param playlist_id The Spotify ID of the playlist - * @param opts Additional options for request - */ -export const getPlaylist = async ( - client: HTTPClient, - playlist_id: string, - opts?: GetPlaylistOpts -) => { - return await client.fetch("/playlists/" + playlist_id, "json", { - query: opts - }); -}; - -export interface ChangePlaylistDetailsBody extends JSONObject { - /** - * The new name for the playlist, for example "My New Playlist Title" - */ - name?: string; - /** - * If true the playlist will be public, if false it will be private. - */ - public?: boolean; - /** - * If true, the playlist will become collaborative and other users will be able to modify the playlist in their Spotify client. - * - * Note: You can only set collaborative to true on non-public playlists. - */ - collaborative?: boolean; - /** - * Value for playlist description as displayed in Spotify Clients and in the Web API. - */ - description?: string; -} - -/** - * Change a playlist's name and public/private state. (The user must, of course, own the playlist.) - * - * @param client Spotify HTTPClient - * @param playlist_id The Spotify ID of the playlist. - * @param body Changes you want to make to the playlist - */ -export const changePlaylistDetails = async ( - client: HTTPClient, - playlist_id: string, - body: ChangePlaylistDetailsBody -) => { - await client.fetch("/playlists/" + playlist_id, "void", { - method: "PUT", - json: body - }); -}; - -export interface GetPlaylistTracksOpts - extends SearchParams, - PlaylistFieldsOpts, - PagingOptions { - /** - * An ISO 3166-1 alpha-2 country code. - * If a country code is specified, only content that is available in that market will be returned. - */ - market?: Market; -} - -/** - * Get full details of the items of a playlist owned by a Spotify user - * - * @param client Spotify HTTPClient - * @param playlist_id The Spotify ID of the playlist. - * @param opts Additional options for request - */ -export const getPlaylistTracks = async ( - client: HTTPClient, - playlist_id: string, - opts?: GetPlaylistTracksOpts -) => { - return await client.fetch>( - `/playlists/${playlist_id}/tracks`, - "json", - { - query: opts - } - ); -}; - -/** - * Add one or more items to a user's playlist - * - * @param client Spotify HTTPClient - * @param playlist_id The Spotify ID of the playlist - * @param uris List of Spotify URIs to add, can be track or episode URIs - * @param position The position to insert the items, a zero-based index - */ -export const addItemsToPlaylist = async ( - client: HTTPClient, - playlist_id: string, - uris: string[], - position?: number -) => { - return await client.fetch( - `/playlists/${playlist_id}/tracks`, - "json", - { - method: "POST", - query: { - uris, - position - } - } - ); -}; - -/** - * Add one item to a user's playlist - * - * @param client Spotify HTTPClient - * @param playlist_id The Spotify ID of the playlist - * @param uri Spotify URIs to add, can be track or episode URIs - * @param position The position to insert the items, a zero-based index - */ -export const addItemToPlaylist = async ( - client: HTTPClient, - playlist_id: string, - uri: string, - position?: number -) => { - return await addItemsToPlaylist(client, playlist_id, [uri], position); -}; - -export interface ReorderPlaylistItemsOpts extends JSONObject { - /** - * The position of the first item to be reordered. - */ - range_start?: number; - /** - * The position where the items should be inserted. - */ - insert_before?: number; - /** - * The amount of items to be reordered. Defaults to 1 if not set. - * The range of items to be reordered begins from the `range_start` position, and includes the `range_length` subsequent items. - */ - range_length?: number; - /** - * The playlist's snapshot ID against which you want to make the changes. - */ - snapshot_id?: string; -} - -/** - * Reorder items in a playlist depending on the request's parameters. - * - * @param client Spotify HTTPClient - * @param playlist_id The Spotify ID of the playlist. - * @param opts Additional options for request - */ -export const reorderPlaylistItems = async ( - client: HTTPClient, - playlist_id: string, - opts?: ReorderPlaylistItemsOpts -) => { - return await client.fetch( - `/playlists/${playlist_id}/tracks`, - "json", - { - method: "PUT", - json: opts - } - ); -}; - -/** - * Replace items in a playlist. Replacing items in a playlist will overwrite its existing items. - * This operation can be used for replacing or clearing items in a playlist. - * - * @param client Spotify HTTPClient - * @param playlist_id The Spotify ID of the playlist. - * @param uris List of Spotify URIs to set, can be track or episode URIs. A maximum of 100 items can be set in one request. - */ -export const replacePlaylistItems = async ( - client: HTTPClient, - playlist_id: string, - uris: string[] -) => { - return await client.fetch( - `/playlists/${playlist_id}/tracks`, - "json", - { - method: "PUT", - json: { uris } - } - ); -}; - -/** - * Remove one or more items from a user's playlist. - * - * @param client Spotify HTTPClient - * @param playlist_id The Spotify ID of the playlist. - * @param uris List of Spotify URIs to set, can be track or episode URIs. A maximum of 100 items can be set in one request. - * @param snapshot_id The playlist's snapshot ID against which you want to make the changes. - */ -export const removePlaylistItems = async ( - client: HTTPClient, - playlist_id: string, - uris: string[], - snapshot_id?: string -) => { - return await client.fetch( - `/playlists/${playlist_id}/tracks`, - "json", - { - method: "DELETE", - json: { - tracks: uris.map((uri) => ({ uri })), - snapshot_id - } - } - ); -}; - -/** - * Remove one item from a user's playlist. - * - * @param client Spotify HTTPClient - * @param playlist_id The Spotify ID of the playlist. - * @param uri Spotify URI to set, can be track or episode URIs. - * @param snapshot_id The playlist's snapshot ID against which you want to make the changes. - */ -export const removePlaylistItem = async ( - client: HTTPClient, - playlist_id: string, - uri: string, - snapshot_id?: string -) => { - return await removePlaylistItems(client, playlist_id, [uri], snapshot_id); -}; - -/** - * Get a list of the playlists owned or followed by the current Spotify user. - * - * @param client Spotify HTTPClient - * @param opts Additional options for request - */ -export const getCurrentUsersPlaylists = async ( - client: HTTPClient, - opts?: PagingOptions -) => { - return await client.fetch>( - "/me/playlists", - "json", - { - query: opts - } - ); -}; - -/** - * Get a list of the playlists owned or followed by a Spotify user. - * - * @param client Spotify HTTPClient - * @param user_id The user's Spotify user ID. - * @param opts Additional options for request - */ -export const getUsersPlaylists = async ( - client: HTTPClient, - user_id: string, - opts?: PagingOptions -) => { - return await client.fetch>( - `/users/${user_id}/playlists`, - "json", - { - query: opts - } - ); -}; - -export interface CreatePlaylistBody extends JSONObject { - /** - * The name for the new playlist, for example "Your Coolest Playlist". This name does not need to be unique; a user may have several playlists with the same name. - */ - name: string; - /** - * Defaults to true. If true the playlist will be public, if false it will be private. To be able to create private playlists, the user must have granted the playlist-modify-private scope - */ - public?: boolean; - /** - * Defaults to false. If true the playlist will be collaborative. Note: to create a collaborative playlist you must also set public to false. To create collaborative playlists you must have granted `playlist-modify-private` and `playlist-modify-public` scopes. - */ - collaborative?: boolean; - /** - * Value for playlist description as displayed in Spotify Clients and in the Web API. - */ - description?: string; -} - -/** - * Create a playlist for a Spotify user. (The playlist will be empty until you add tracks.) - * - * @param client Spotify HTTPClient - * @param user_id The user's Spotify user ID. - * @param body Data that will be assinged to new playlist - */ -export const createPlaylist = async ( - client: HTTPClient, - user_id: string, - body: CreatePlaylistBody -) => { - return await client.fetch(`/users/${user_id}/playlists`, "json", { - json: body, - method: "POST" - }); -}; - -export interface GetFeaturedPlaylistsOpts extends SearchParams, PagingOptions { - /** - * A country: an ISO 3166-1 alpha-2 country code. Provide this parameter if you want the list of returned items to be relevant to a particular country. If omitted, the returned items will be relevant to all countries. - */ - country?: string; - /** - * The desired language, consisting of a lowercase ISO 639-1 language code and an uppercase ISO 3166-1 alpha-2 country code, joined by an underscore. For example: es_MX, meaning "Spanish (Mexico)". Provide this parameter if you want the results returned in a particular language (where available). - * - * @example "sv_SE" - */ - locale?: string; - /** - * A timestamp in ISO 8601 format: yyyy-MM-ddTHH:mm:ss. Use this parameter to specify the user's local time to get results tailored for that specific date and time in the day. If not provided, the response defaults to the current UTC time. Example: "2014-10-23T09:00:00" for a user whose local time is 9AM. If there were no featured playlists (or there is no data) at the specified time, the response will revert to the current UTC time. - * - * @example "2014-10-23T09:00:00" - */ - timestamp: string; -} - -/** - * Get a list of Spotify featured playlists (shown, for example, on a Spotify player's 'Browse' tab). - * - * @param client Spotify HTTPClient - * @param opts Additional options for request - */ -export const getFeaturedPlaylists = async ( - client: HTTPClient, - opts?: GetFeaturedPlaylistsOpts -) => { - return await client.fetch( - "/browse/featured-playlists", - "json", - { - query: opts - } - ); -}; - -export interface GetCategorysPlaylistsOpts extends SearchParams, PagingOptions { - /** - * A country: an ISO 3166-1 alpha-2 country code. Provide this parameter to ensure that the category exists for a particular country. - * @example "SE" - */ - country: string; -} - -/** - * Get a list of Spotify playlists tagged with a particular category. - * - * @param client Spotify HTTPClient - * @param category_id The Spotify category ID for the category. - * @param opts Additional options for request - */ -export const getCategorysPlaylists = async ( - client: HTTPClient, - category_id: string, - opts?: GetCategorysPlaylistsOpts -) => { - return await client.fetch( - `/browse/categories/${category_id}/playlists`, - "json", - { - query: opts - } - ); -}; - -/** - * Get the current image associated with a specific playlist. - * - * @param client Spotify HTTPClient - */ -export const getPlaylistCoverImage = async ( - client: HTTPClient, - playlist_id: string -) => { - return await client.fetch[]>( - `/playlists/${playlist_id}/images`, - "json" - ); -}; - -/** - * Upload custom images to the playlist. - * - * @param playlist_id The Spotify ID of the playlist. - * @param image The image should contain a Base64 encoded JPEG image data, maximum payload size is 256 KB. - */ -export const uploadPlaylistCoverImage = async ( - client: HTTPClient, - playlist_id: string, - image: string -) => { - return await client.fetch(`/playlists/${playlist_id}/images`, "void", { - method: "PUT", - headers: { - "Content-Type": "image/jpeg" - }, - body: image - }); -}; diff --git a/src/api/playlist/playlist.types.ts b/src/api/playlist/playlist.types.ts deleted file mode 100644 index ab4412c..0000000 --- a/src/api/playlist/playlist.types.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { ExternalUrls, Followers, Image, PagingObject } from "../general.types"; -import { UserPublic } from "../user/user.types"; -import { Track } from "../track/track.types"; -import { JSONObject } from "../../shared"; - -export type SnapshotResponse = { snapshot_id: string }; - -export interface PlaylistSimplified extends JSONObject { - /** - * `true` if the owner allows other users to modify the playlist. - */ - collaborative: boolean; - /** - * The playlist description. Only returned for modified, verified playlists, otherwise `null`. - */ - description: string | null; - /** - * Known external URLs for this playlist. - */ - external_urls: ExternalUrls; - - /** - * A link to the Web API endpoint providing full details of the playlist. - */ - href: string; - /** - * The Spotify ID for the playlist. - */ - id: string; - /** - * Images for the playlist. - * The array may be empty or contain up to three images. - * The images are returned by size in descending order. - * - * Be aware that the links will expire in less than one day. - */ - images: Image[]; - /** - * The name of the playlist. - */ - name: string; - /** - * The user who owns the playlist - */ - owner: UserPublic; - - /** - * The version identifier for the current playlist. - * Can be supplied in other requests to target a specific playlist version. - */ - snapshot_id: string; - /** - * The object type: "playlist" - */ - type: "playlist"; - /** - * The Spotify URI for the playlist. - */ - uri: string; - /** A collection containing a link ( href ) to the Web API endpoint where full details of the playlist’s tracks can be retrieved, along with the total number of tracks in the playlist. */ - tracks: PlaylistTracksReference; -} - -export interface PlaylistTracksReference extends JSONObject { - /** - * A link to the Web API endpoint where full details of the playlist’s tracks can be retrieved. - */ - href: string; - /** The total number of tracks in playlist. */ - total: number; -} - -/** - * The structure containing the details of the Spotify Track in the playlist. - */ -export interface PlaylistTrack extends JSONObject { - /** - * The date and time the track or episode was added. - * Note: some very old playlists may return null in this field. - */ - added_at: string | null; - /** - * The Spotify user who added the track or episode. - * Note: some very old playlists may return null in this field. - */ - added_by: UserPublic | null; - /** - * Whether this track or episode is a local file or not. - */ - is_local: boolean; - track: Track; -} - -export interface Playlist extends PlaylistSimplified, JSONObject { - /** - * Information about the followers of the playlist. - */ - followers: Followers; - /** - * The playlist's public/private status: - * - * `true` => the playlist is public \ - * `false` => the playlist is private \ - * `null` => the playlist status is not relevant - */ - public: boolean | null; - /** - * The tracks of the playlist. - */ - tracks: PagingObject; -} - -export interface FeaturedPlaylists extends JSONObject { - /** The message from the featured playlists. */ - message: string; - /** - * The list of the featured playlists wrapped in Paging object. - */ - playlists: PagingObject; -} diff --git a/src/api/search/index.ts b/src/api/search/index.ts deleted file mode 100644 index 6baffab..0000000 --- a/src/api/search/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { type SearchOpts, search } from "./search.endpoints"; -export * from "./search.types"; diff --git a/src/api/search/search.endpoints.ts b/src/api/search/search.endpoints.ts deleted file mode 100644 index c5c421a..0000000 --- a/src/api/search/search.endpoints.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { SearchParams } from "../../shared"; -import { Market } from "../market/market.types"; -import { - SearchFilters, - SearchResponse, - SearchType, - SearchTypeLiteral -} from "../search/search.types"; -import { HTTPClient } from "../client"; - -export interface SearchOpts extends SearchParams { - /** - * If `include_external=audio` is specified it signals that the client can play externally hosted audio content, and marks the content as playable in the response. - * - * By default externally hosted audio content is marked as unplayable in the response. - */ - include_external?: "audio"; - /** - * The maximum number of results to return in each item type. - * Minimum: 1. Maximum: 50. - * - * @default 20 - */ - limit?: number; - /** - * An ISO 3166-1 alpha-2 country code. - * If a country code is specified, only content that is available in that market will be returned. - */ - market?: Market; - /** - * The index of the first result to return. Use with limit to get the next page of search results. - * Minimum 0. Maximum 1000. - * - * @default 0 - */ - offset?: number; -} - -// Specific type that is used to create spotify api object with correct signature for search function -export interface SearchEndpoint { - search( - query: string | SearchFilters, - type: T, - opts?: SearchOpts - ): Promise>>; -} - -/** - * Get Spotify catalog information about albums, artists, playlists, tracks, shows, episodes or audiobooks that match a keyword string. - * - * @param client Spotify HTTPClient - * @param query Your search query - * @param type One or multiple item types to search across - * @param opts Additional options for request - */ -export const search = async ( - client: HTTPClient, - query: string | SearchFilters, - type: T, - opts?: SearchOpts -): Promise>> => { - let q = ""; - if (typeof query === "string") { - q = query; - } else { - for (const key in query) { - q += key === "q" ? query[key] : `${key}:${query[key]}`; - } - } - - return await client.fetch("/search", "json", { - query: { q, type, ...opts } - }); -}; diff --git a/src/api/search/search.types.ts b/src/api/search/search.types.ts deleted file mode 100644 index 5a80534..0000000 --- a/src/api/search/search.types.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { PagingObject } from "../general.types"; -import { AlbumSimplified } from "../album/album.types"; -import { Artist } from "../artist/artist.types"; -import { Track } from "../track/track.types"; -import { PlaylistSimplified } from "../playlist/playlist.types"; -import { JSONObject, SearchParams } from "../../shared"; - -/** - * Item types to search across. - */ -export type SearchType = "album" | "artist" | "playlist" | "track"; -// | "show" -// | "episode" -// | "audiobook" - -type SearchResponseType = "albums" | "artists" | "playlists" | "tracks"; - -interface SearchResponseTypeMap extends Record { - album: "albums"; - artist: "artists"; - playlist: "playlists"; - track: "tracks"; -} - -export type SearchTypeLiteral = - T extends SearchType[] - ? Pick[T[number]] - : T extends SearchType - ? SearchResponseTypeMap[T] - : never; - -type SearchEntity = Track | Artist | AlbumSimplified | PlaylistSimplified; - -export interface SearchResponse - extends JSONObject, - Record> { - tracks: PagingObject; - artists: PagingObject; - albums: PagingObject; - playlists: PagingObject; -} - -export interface SearchFilters extends SearchParams { - q?: string; - track?: string; - artist?: string; - album?: string; - year?: number | string; - genre?: string; - upc?: number | string; - isrc?: number | string; - tag?: "hipster" | "new"; -} diff --git a/src/api/show/index.ts b/src/api/show/index.ts deleted file mode 100644 index c13c2f7..0000000 --- a/src/api/show/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./show.endpoints"; -export * from "./show.types"; diff --git a/src/api/show/show.endpoints.ts b/src/api/show/show.endpoints.ts deleted file mode 100644 index fff62e3..0000000 --- a/src/api/show/show.endpoints.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { HTTPClient } from "../client"; -import { EpisodeSimplified } from "../episode/episode.types"; -import { PagingObject, PagingOptions } from "../general.types"; -import { Market } from "../market/market.types"; -import { Show, ShowSimplified } from "./show.types"; - -/** - * Get spotify catalog information for a single show by its unique Spotify ID. - * - * @param client Spotify HTTPClient - * @param show_id The Spotify ID of the show - * @param market An ISO 3166-1 alpha-2 country code - */ -export const getShow = async ( - client: HTTPClient, - show_id: string, - market?: Market -) => { - return await client.fetch("/shows/" + show_id, "json", { - query: { market } - }); -}; - -/** - * Get spotify catalog information for multiple shows by their Spotify IDs. - * - * @param client Spotify HTTPClient - * @param show_ids List of the Spotify IDs for the shows. Maximum: 20 IDs - * @param market An ISO 3166-1 alpha-2 country code - */ -export const getShows = async ( - client: HTTPClient, - show_ids: string[], - market?: Market -) => { - return ( - await client.fetch<{ shows: ShowSimplified }>("/shows", "json", { - query: { market, ids: show_ids } - }) - ).shows; -}; - -export interface GetShowEpisodesOpts extends PagingOptions { - /** - * An ISO 3166-1 alpha-2 country code. - * If a country code is specified, only content that is available in that market will be returned. - */ - market?: Market; -} - -/** - * Get Spotify catalog information about an show's episodes. - * Optional parameters can be used to limit the number of episodes returned. - * - * @param client Spotify HTTPClient - * @param show_id The Spotify ID of the show - * @param opts Additional option for request - */ -export const getShowEpisodes = async ( - client: HTTPClient, - show_id: string, - opts?: GetShowEpisodesOpts -) => { - return await client.fetch>( - `/shows/${show_id}/episodes`, - "json", - { - query: opts - } - ); -}; - -export interface GetSavedShowsOpts extends PagingOptions { - /** - * An ISO 3166-1 alpha-2 country code. - * If a country code is specified, only content that is available in that market will be returned. - */ - market?: Market; -} - -/** - * Get a list of shows saved in the current Spotify user's library. - * - * @param client Spotify HTTPClient - * @param opts Additional option for request - */ -export const getSavedShows = async ( - client: HTTPClient, - opts?: GetSavedShowsOpts -) => { - return await client.fetch< - PagingObject<{ - /** - * The date and time the album was saved Timestamps are returned in ISO 8601 format as Coordinated Universal Time (UTC) with a zero offset: YYYY-MM-DDTHH:MM:SSZ. - */ - added_at: string; - /** - * Information about the show. - */ - show: Show; - }> - >("/me/shows", "json", { query: opts }); -}; - -/** - * Save one or more shows to current Spotify user's library. - * - * @param client Spotify HTTPClient - * @param shows_ids List of the Spotify IDs for the shows. Maximum: 20 - */ -export const saveShows = async (client: HTTPClient, shows_ids: string[]) => { - await client.fetch("/me/shows", "void", { - method: "PUT", - query: { ids: shows_ids } - }); -}; - -/** - * Save show to current Spotify user's library. - * - * @param client Spotify HTTPClient - * @param show_id The Spotify ID of the show - */ -export const saveShow = async (client: HTTPClient, show_id: string) => { - await saveShows(client, [show_id]); -}; - -/** - * Delete one or more shows from current Spotify user's library. - * - * @param client Spotify HTTPClient - * @param show_ids List of the Spotify IDs for the shows. Maximum: 20 - */ -export const removeSavedShows = async ( - client: HTTPClient, - show_ids: string[] -) => { - await client.fetch("/me/shows", "void", { - method: "DELETE", - query: { - ids: show_ids - } - }); -}; - -/** - * Remove show from the current user's 'Your Shows' library. - * - * @param client Spotify HTTPClient - * @param show_id The Spotify ID of the show - */ -export const removeSavedShow = async (client: HTTPClient, show_id: string) => { - await removeSavedShows(client, [show_id]); -}; - -/** - * Check if one or more shows is already saved in the current Spotify users' 'Your Shows' library. - * - * @param client Spotify HTTPClient - * @param show_ids List of the Spotify IDs for the shows. Maximum: 20 - */ -export const checkSavedShows = async ( - client: HTTPClient, - show_ids: string[] -) => { - return await client.fetch("/me/shows/contains", "json", { - query: { - ids: show_ids - } - }); -}; - -/** - * Check if show is already saved in the current Spotify user's 'Your Shows' library. - * - * @param client Spotify HTTPClient - * @param show_id The Spotify ID of the show - */ -export const checkSavedShow = async (client: HTTPClient, show_id: string) => { - return (await checkSavedShows(client, [show_id]))[0]; -}; diff --git a/src/api/show/show.types.ts b/src/api/show/show.types.ts deleted file mode 100644 index d01ff2d..0000000 --- a/src/api/show/show.types.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { JSONObject } from "../../shared"; -import { EpisodeSimplified } from "../episode/episode.types"; -import { Copyright, ExternalUrls, Image, PagingObject } from "../general.types"; -import { Market } from "../market/market.types"; - -export interface ShowSimplified extends JSONObject { - /** - * A list of the countries in which the track can be played. - */ - available_markets: Market[]; - /** - * The copyright statements of the show. - */ - copyrights: Copyright[]; - /** - * The description of the show without html tags. - */ - description: string; - /** - * The description of the show with html tags. - */ - html_description: string; - /** - * Whether or not the show has explicit lyrics. - */ - explicit: boolean; - /** - * External URLs for this show. - */ - external_urls: ExternalUrls; - /** - * A link to the Web API endpoint providing full details of the show. - */ - href: string; - /** - * The Spotify ID for the show. - */ - id: string; - /** - * Images of the show in various sizes, widest first. - */ - images: Image[]; - /** - * True, if the episode is hosted outside of Spotify's CDN. - */ - is_externally_hosted: boolean; - /** - * A list of the languages used in the episode, identified by their ISO 639-1 code. - */ - languages: string[]; - /** - * The media type of the show. - */ - media_type: string; - /** - * The name of the episode. - */ - name: string; - /** - * The publisher of the show. - */ - publisher: string; - /** - * THe object type. - */ - type: "show"; - /** - * The Spotify URI for the show. - */ - uri: string; - /** - * The total number of episodes in the show. - */ - total_episodes: number; -} - -export interface Show extends ShowSimplified, JSONObject { - /** - * The episodes of the show. - */ - episodes: PagingObject; -} diff --git a/src/api/track/index.ts b/src/api/track/index.ts deleted file mode 100644 index 3bf5b95..0000000 --- a/src/api/track/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./track.endpoints"; -export * from "./track.types"; diff --git a/src/api/track/track.endpoints.ts b/src/api/track/track.endpoints.ts deleted file mode 100644 index c784c65..0000000 --- a/src/api/track/track.endpoints.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { HTTPClient } from "../client"; -import { Market } from "../market/market.types"; -import { PagingObject, PagingOptions } from "../general.types"; -import { - AudioAnalysis, - AudioFeatures, - GetRecommendationsOpts, - Recomendations, - Track -} from "./track.types"; - -/** - * Get Spotify catalog information for a single track identified - * by its unique Spotify ID. - * - * @param client Spotify HTTPClient - * @param track_id The Spotify ID for the track - * @param market An ISO 3166-1 alpha-2 country code - */ -export const getTrack = async ( - client: HTTPClient, - track_id: string, - market?: Market -) => { - return await client.fetch("/tracks/" + track_id, "json", { - query: { market } - }); -}; - -/** - * Get Spotify catalog information for multiple tracks based on their Spotify IDs. - * - * @param client Spotify HTTPClient - * @param track_ids List of Spotify track IDs. Maximum 50 IDs - * @param market An ISO 3166-1 alpha-2 country code - */ -export const getTracks = async ( - client: HTTPClient, - track_ids: string[], - market?: Market -) => { - return ( - await client.fetch<{ tracks: Track[] }>("/tracks", "json", { - query: { - ids: track_ids, - market - } - }) - ).tracks; -}; - -export interface GetSavedTracksOpts extends PagingOptions { - /** - * An ISO 3166-1 alpha-2 country code. - */ - marker?: Market; -} - -/** - * Get a list of the songs saved in the current - * Spotify user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param opts Additional option for request - */ -export const getSavedTracks = async ( - client: HTTPClient, - opts: GetSavedTracksOpts -) => { - return await client.fetch>("/me/tracks", "json", { - query: opts - }); -}; - -/** - * Save one or more tracks to the current user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param track_ids List of the Spotify track IDs. Maximum 50 IDs - */ -export const saveTracks = async (client: HTTPClient, track_ids: string[]) => { - await client.fetch("/me/tracks", "void", { - method: "PUT", - query: { - ids: track_ids - } - }); -}; - -/** - * Save track to the current user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param track_id Spotify track ID - */ -export const saveTrack = async (client: HTTPClient, track_id: string) => { - await saveTracks(client, [track_id]); -}; - -/** - * Remove one or more tracks from the current user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param track_ids List of the Spotify track IDs. Maximum 50 IDs - */ -export const removeSavedTracks = async ( - client: HTTPClient, - track_ids: string[] -) => { - await client.fetch("/me/tracks", "void", { - method: "DELETE", - query: { - ids: track_ids - } - }); -}; - -/** - * Remove track from the current user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param track_id Spotify track ID - */ -export const removeSavedTrack = async ( - client: HTTPClient, - track_id: string -) => { - await removeSavedTracks(client, [track_id]); -}; - -/** - * Check if one or more tracks is already saved in the current Spotify user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param track_ids List of the Spotify track IDs. Maximum 50 IDs - */ -export const checkSavedTracks = async ( - client: HTTPClient, - track_ids: string[] -) => { - return await client.fetch("/me/tracks/contains", "json", { - query: { - ids: track_ids - } - }); -}; - -/** - * Check if track is already saved in the current Spotify user's 'Your Music' library. - * - * @param client Spotify HTTPClient - * @param track_id Spotify track ID - */ -export const checkSavedTrack = async (client: HTTPClient, track_id: string) => { - return (await checkSavedTracks(client, [track_id]))[0]; -}; - -/** - * Get audio features for multiple tracks based on their Spotify IDs. - * - * @param client Spotify HTTPClient - * @param track_ids List of the Spotify track IDs. Maximum 100 IDs - */ -export const getTracksAudioFeatures = async ( - client: HTTPClient, - track_ids: string[] -) => { - return ( - await client.fetch<{ audio_features: AudioFeatures[] }>( - "/audio-features", - "json", - { - query: { - ids: track_ids - } - } - ) - ).audio_features; -}; - -/** - * Get audio features for a track based on its Spotify ID. - * - * @param client Spotify HTTPClient - * @param track_id Spotify track ID - */ -export const getTrackAudioFeatures = async ( - client: HTTPClient, - track_id: string -) => { - return await client.fetch( - "/audio-features/" + track_id, - "json" - ); -}; - -/** - * Get a low-level audio analysis for a track in the Spotify catalog. - * The audio analysis describes the track’s structure and musical content, including rhythm, pitch, and timbre. - * - * @param client Spotify HTTPClient - * @param track_id Spotify track ID - */ -export const getTracksAudioAnalysis = async ( - client: HTTPClient, - track_id: string -) => { - return await client.fetch( - "/audio-analysis/" + track_id, - "json" - ); -}; - -/** - * Recommendations are generated based on the available information for a given seed entity and matched against similar artists and tracks. If there is sufficient information about the provided seeds, a list of tracks will be returned together with pool size details. - - * @param client Spotify HTTPClient - * @param opts Options and seeds for recomendations - */ -export const getRecommendations = async ( - client: HTTPClient, - opts: GetRecommendationsOpts -) => { - return await client.fetch("/recommendations", "json", { - query: opts - }); -}; diff --git a/src/api/track/track.types.ts b/src/api/track/track.types.ts deleted file mode 100644 index 21e0b08..0000000 --- a/src/api/track/track.types.ts +++ /dev/null @@ -1,690 +0,0 @@ -import { JSONObject, SearchParams } from "../../shared"; -import { AlbumSimplified } from "../album/album.types"; -import { Artist } from "../artist/artist.types"; -import { Genre } from "../genre/genre.types"; -import { Market } from "../market/market.types"; -import { ArtistSimplified } from "../artist/artist.types"; -import { - ExternalIds, - ExternalUrls, - RestrictionsReason -} from "../general.types"; - -export interface LinkedTrack extends JSONObject { - /** - * A map of url name and the url. - */ - external_urls: ExternalUrls; - /** - * The api url where you can get the full details of the linked track. - */ - href: string; - /** - * The Spotify ID for the track. - */ - id: string; - /** - * The object type: "track". - */ - type: "track"; - /** - * The Spotify URI for the track. - */ - uri: string; -} - -export interface TrackSimplified extends JSONObject { - /** - * The artists who performed the track. - */ - artists: ArtistSimplified[]; - /** - * A list of the countries in which the track can be played. - */ - available_markets: Market[]; - /** - * The disc number (usually 1 unless the album consists of more than one disc). - */ - disc_number: number; - /** - * The track length in milliseconds. - */ - duration_ms: number; - /** - * Whether or not the track has explicit lyrics. - */ - explicit: boolean; - /** External URLs for this track. */ - external_urls: ExternalUrls; - /** - * A link to the Web API endpoint providing full details of the track. - */ - href: string; - /** - * Whether or not the track is from a local file. - */ - is_local: boolean; - /** - * If true, the track is playable in the given market. - * Otherwise false. - */ - is_playable?: boolean; - /** - * Part of the response when Track Relinking is applied and is only part of the response if the track linking, in fact, exists. - */ - linked_from?: LinkedTrack; - /** - * The name of the track. - */ - name: string; - /** - * A link to a 30 second preview (MP3 format) of the track. - */ - preview_url: string | null; - /** - * Included in the response when a content restriction is applied. - */ - restrictions?: { - reason: RestrictionsReason; - }; - /** - * The number of the track. If an album has several discs, the track number is the number on the specified disc. - */ - track_number: number; - /** - * The Spotify ID for the track. - */ - id: string; - /** - * The object type: "track". - */ - type: "track"; - /** - * The Spotify URI for the track. - */ - uri: string; -} - -export interface Track extends TrackSimplified, JSONObject { - /** - * The album on which the track appears. - */ - album: AlbumSimplified; - /** - * The artists who performed the track. - */ - artists: Artist[]; - /** - * Known external IDs for the track. - */ - external_ids: ExternalIds; - /** - * The popularity of the track. - * The value will be between 0 and 100, with 100 being the most popular. - */ - popularity: number; -} - -export interface AudioFeatures extends JSONObject { - /** - * A confidence measure from 0.0 to 1.0 of whether the track is acoustic. 1.0 represents high confidence the track is acoustic. - */ - acousticness: number; - /** - * A URL to access the full audio analysis of this track. An access token is required to access this data. - */ - analysis_url: string; - /** - * Danceability describes how suitable a track is for dancing based on a combination of musical elements including tempo, rhythm stability, beat strength, and overall regularity. - * A value of 0.0 is least danceable and 1.0 is most danceable. - */ - danceability: number; - /** - * The duration of the track in milliseconds. - */ - duration_ms: number; - /** - * Energy is a measure from 0.0 to 1.0 and represents a perceptual measure of intensity and activity. - * Typically, energetic tracks feel fast, loud, and noisy. For example, death metal has high energy, while a Bach prelude scores low on the scale. - * Perceptual features contributing to this attribute include dynamic range, perceived loudness, timbre, onset rate, and general entropy. - */ - energy: number; - /** - * The Spotify ID for the track. - */ - id: string; - /** - * Predicts whether a track contains no vocals. "Ooh" and "aah" sounds are treated as instrumental in this context. - * Rap or spoken word tracks are clearly "vocal". The closer the instrumentalness value is to 1.0, the greater likelihood the track contains no vocal content. - * Values above 0.5 are intended to represent instrumental tracks, but confidence is higher as the value approaches 1.0. - */ - instrumentalness: number; - /** - * The key the track is in. Integers map to pitches using standard Pitch Class notation. - * E.g. 0 = C, 1 = C♯/D♭, 2 = D, and so on. - */ - key: number; - /** - * Detects the presence of an audience in the recording. Higher liveness values represent an increased probability that the track was performed live. - * A value above 0.8 provides strong likelihood that the track is live. - */ - liveness: number; - /** - * The overall loudness of a track in decibels (dB). Loudness values are averaged across the entire track and are useful for comparing relative loudness of tracks. - * Loudness is the quality of a sound that is the primary psychological correlate of physical strength (amplitude). Values typical range between -60 and 0 db. - */ - loudness: number; - /** - * Mode indicates the modality (major or minor) of a track, the type of scale from which its melodic content is derived. - * Major is represented by 1 and minor is 0. - */ - mode: number; - /** - * Speechiness detects the presence of spoken words in a track. The more exclusively speech-like the recording (e.g. Talk show, audio book, poetry), the closer to 1.0 the attribute value. - * Values above 0.66 describe tracks that are probably made entirely of spoken words. Values between 0.33 and 0.66 describe tracks that may contain both music and speech, either in sections or layered, including such cases as rap music. - * Values below 0.33 most likely represent music and other non-speech-like tracks. - */ - speechiness: number; - /** - * The overall estimated tempo of a track in beats per minute (BPM). In musical terminology, tempo is the speed or pace of a given piece and derives directly from the average beat duration. - */ - tempo: number; - /** - * An estimated overall time signature of a track. The time signature (meter) is a notational convention to specify how many beats are in each bar (or measure). - */ - time_signature: number; - /** - * A link to the Web API endpoint providing full details of the track. - */ - track_href: string; - /** - * The object type. - */ - type: "audio_features"; - /** - * The Spotify URI for the track. - */ - uri: string; - /** - * A measure from 0.0 to 1.0 describing the musical positiveness conveyed by a track. Tracks with high valence sound more positive (e.g. Happy, cheerful, euphoric), while tracks with low valence sound more negative (e.g. Sad, depressed, angry). - */ - valence: number; -} - -interface AudioAnalysisMeta extends JSONObject { - /** - * The version of the Analyzer used to analyze this track. - */ - analyzer_version: string; - /** - * The platform used to read the track's audio data. - */ - platform: string; - /** - * A detailed status code for this track. If analysis data is missing, this code may explain why. - */ - detailed_status: string; - /** - * The return code of the analyzer process. - * 0 if successful, 1 if any errors occurred. - */ - status_code: 0 | 1; - /** - * The Unix timestamp (in seconds) at which this track was analyzed. - */ - timestamp: number; - /** - * The amount of time taken to analyze this track. - */ - analysis_time: number; - /** - * The method used to read the track's audio data. - */ - input_process: string; -} - -interface AudioAnalysisTrack extends JSONObject { - /** - * The exact number of audio samples analyzed from this track. - */ - num_samples: number; - /** - * Length of the track in seconds. - */ - duration: number; - /** - * This field will always contain the empty string. - */ - sample_md5: ""; - /** - * An offset to the start of the region of the track that was analyzed. - * (As the entire track is analyzed, this should always be 0.) - */ - offset_seconds: number; - /** - * The length of the region of the track was analyzed, if a subset of the track was analyzed. - * (As the entire track is analyzed, this should always be 0.) - */ - window_seconds: number; - /** - * The sample rate used to decode and analyze this track. - * May differ from the actual sample rate of this track available on Spotify. - */ - analysis_sample_rate: number; - /** - * The number of channels used for analysis. - * If 1, all channels are summed together to mono before analysis. - */ - analysis_channels: number; - /** - * The time, in seconds, at which the track's fade-in period ends. - * If the track has no fade-in, this will be 0.0. - */ - end_of_fade_in: number; - /** - * The time, in seconds, at which the track's fade-out period starts. - * If the track has no fade-out, this should match the track's length. - */ - start_of_fade_out: number; - /** - * The overall loudness of a track in decibels (dB). - * Loudness values are averaged across the entire track and are useful for comparing relative loudness of tracks. Loudness is the quality of a sound that is the primary psychological correlate of physical strength (amplitude). - * - * Values typically range between -60 and 0 db. - */ - loudness: number; - /** - * The overall estimated tempo of a track in beats per minute (BPM). - * In musical terminology, tempo is the speed or pace of a given piece and derives directly from the average beat duration. - */ - tempo: number; - /** - * The confidence, from 0.0 to 1.0, of the reliability of the `tempo`. - */ - tempo_confidence: number; - /** - * An estimated time signature. - * The time signature (meter) is a notational convention to specify how many beats are in each bar (or measure). The time signature ranges from 3 to 7 indicating time signatures of "3/4", to "7/4". - */ - time_signature: number; - /** - * The confidence, from 0.0 to 1.0, of the reliability of the `time_signature`. - */ - time_signature_confidence: number; - /** - * The key the track is in. - * Integers map to pitches using standard Pitch Class notation. - * E.g. 0 = C, 1 = C♯/D♭, 2 = D, and so on. - * If no key was detected, the value is -1. - */ - key: number; - /** - * The confidence, from 0.0 to 1.0, of the reliability of the `key`. - */ - key_confidence: number; - /** - * Mode indicates the modality (major or minor) of a track, the type of scale from which its melodic content is derived. - * Major is represented by 1 and minor is 0. - */ - mode: number; - /** - * The confidence, from 0.0 to 1.0, of the reliability of the `mode`. - */ - mode_confidence: number; - /** - * An Echo Nest Musical Fingerprint (ENMFP) codestring for this track. - */ - codestring: string; - /** - * A version number for the Echo Nest Musical Fingerprint format used in the codestring field. - */ - code_version: number; - /** - * An EchoPrint codestring for this track. - */ - echoprintstring: string; - /** - * A version number for the EchoPrint format used in the echoprintstring field. - */ - echoprint_version: number; - /** - * A Synchstring for this track. - */ - synchstring: string; - /** - * A version number for the Synchstring used in the synchstring field. - */ - synch_version: number; - /** - * A Rhythmstring for this track. - * The format of this string is similar to the Synchstring. - */ - rhythmstring: string; - /** - * A version number for the Rhythmstring used in the rhythmstring field. - */ - rhythm_version: number; -} - -interface TimeInterval extends JSONObject { - /** - * The starting point (in seconds) of the time interval. - */ - start: number; - /** - * The duration (in seconds) of the time interval. - */ - duration: number; - /** - * The confidence, from 0.0 to 1.0, of the reliability of the interval. - */ - confidence: number; -} - -interface AudioAnalysisSection extends TimeInterval, JSONObject { - /** - * The overall loudness of the section in decibels (dB). - * Loudness values are useful for comparing relative loudness of sections within tracks. - */ - loudness: number; - /** - * The overall estimated tempo of the section in beats per minute (BPM). - * In musical terminology, tempo is the speed or pace of a given piece and derives directly from the average beat duration. - */ - tempo: number; - /** - * The confidence, from 0.0 to 1.0, of the reliability of the `tempo`. - * Some tracks contain tempo changes or sounds which don't contain tempo (like pure speech) which would correspond to a low value in this field. - */ - tempo_confidence: number; - /** - * The estimated overall key of the section. - * The values in this field ranging from 0 to 11 mapping to pitches using standard Pitch Class notation - * (E.g. 0 = C, 1 = C♯/D♭, 2 = D, and so on). - * If no key was detected, the value is -1. - */ - key: number; - /** - * The confidence, from 0.0 to 1.0, of the reliability of the `key`. - * Songs with many key changes may correspond to low values in this field. - */ - key_confidence: number; - /** - * Indicates the modality (major or minor) of a section, the type of scale from which its melodic content is derived. - * This field will contain a 0 for "minor", a 1 for "major", or a -1 for no result. - */ - mode: number; - /** - * The confidence, from 0.0 to 1.0, of the reliability of the `mode`. - */ - mode_confidence: number; - /** - * An estimated time signature. The time signature (meter) is a notational convention to specify how many beats are in each bar (or measure). - * The time signature ranges from 3 to 7 indicating time signatures of "3/4", to "7/4". - */ - time_signature: number; - /** - * The confidence, from 0.0 to 1.0, of the reliability of the time_signature. - * Sections with time signature changes may correspond to low values in this field. - */ - time_signature_confidence: number; -} - -interface AudioAnalysisSegment extends TimeInterval, JSONObject { - /** - * The onset loudness of the segment in decibels (dB). - */ - loudness_start: number; - /** - * The peak loudness of the segment in decibels (dB). - */ - loudness_max_time: number; - /** - * The segment-relative offset of the segment peak loudness in seconds. - */ - loudness_max: number; - /** - * The offset loudness of the segment in decibels (dB). This value should be equivalent to the loudness_start of the following segment. - */ - loudness_end: number; - /** - * Pitch content is given by a “chroma” vector, corresponding to the 12 pitch classes C, C#, D to B, with values ranging from 0 to 1 that describe the relative dominance of every pitch in the chromatic scale. - */ - pitches: number[]; - /** - * Timbre is the quality of a musical note or sound that distinguishes different types of musical instruments, or voices. - * It is a complex notion also referred to as sound color, texture, or tone quality, and is derived from the shape of a segment’s spectro-temporal surface, independently of pitch and loudness. - */ - timbre: number[]; -} - -export interface AudioAnalysis extends JSONObject { - meta: AudioAnalysisMeta; - track: AudioAnalysisTrack; - /** - * The time intervals of the bars throughout the track. - * A bar (or measure) is a segment of time defined as a given number of beats. - */ - bars: TimeInterval[]; - /** - * The time intervals of beats throughout the track. - * A beat is the basic time unit of a piece of music; for example, each tick of a metronome. Beats are typically multiples of tatums. - */ - beats: TimeInterval[]; - /** - * Sections are defined by large variations in rhythm or timbre, e.g. chorus, verse, bridge, guitar solo, etc. Each section contains its own descriptions of `tempo`, `key`, `mode`, `time_signature`, and `loudness`. - */ - sections: AudioAnalysisSection[]; - /** - * Each segment contains a roughly conisistent sound throughout its duration. - */ - segments: AudioAnalysisSegment[]; - /** - * A tatum represents the lowest regular pulse train that a listener intuitively infers from the timing of perceived musical events (segments). - */ - tatums: TimeInterval[]; -} - -export interface GetRecommendationsOpts extends SearchParams { - /** - * List of Spotify IDs for seed artists. Maximum 5 IDs - */ - seed_artists: string[]; - /** - * List of any genres in the set of available genre seeds. Maximum 5 genres - */ - seed_genres: Genre[]; - /** - * List of Spotify IDs for a seed track. Maximum 5 IDs - */ - seed_tracks: string[]; - /** - * The target size of the list of recommended tracks. - * Minimum: 1. Maximum: 100. Default: 20. - */ - limit?: number; - /** - * An ISO 3166-1 alpha-2 country code - */ - market?: Market; - /** - * Range: `>= 0 <= 1` - */ - max_acousticness?: number; - /** - * Range: `>= 0 <= 1` - */ - max_danceability?: number; - max_duration_ms?: number; - /** - * Range: `>= 0 <= 1` - */ - max_energy?: number; - /** - * Range: `>= 0 <= 1` - */ - max_instrumentalness?: number; - /** - * Range: `>= 0 <= 11` - */ - max_key?: number; - /** - * Range: `>= 0 <= 1` - */ - max_liveness?: number; - max_loudness?: number; - /** - * Range: `>= 0 <= 1` - */ - max_mode?: number; - /** - * Range `>= 0 <= 100` - */ - max_popularity?: number; - /** - * Range: `>= 0 <= 1` - */ - max_speechiness?: number; - max_tempo?: number; - max_time_signature?: number; - /** - * Range: `>= 0 <= 1` - */ - max_valence?: number; - /** - * Range: `>= 0 <= 1` - */ - min_acousticness?: number; - /** - * Range: `>= 0 <= 1` - */ - min_danceability?: number; - min_duration_ms?: number; - /** - * Range: `>= 0 <= 1` - */ - min_energy?: number; - /** - * Range: `>= 0 <= 1` - */ - min_instrumentalness?: number; - /** - * Range: `>= 0 <= 11` - */ - min_key?: number; - /** - * Range: `>= 0 <= 1` - */ - min_liveness?: number; - min_loudness?: number; - /** - * Range: `>= 0 <= 1` - */ - min_mode?: number; - /** - * Range `>= 0 <= 100` - */ - min_popularity?: number; - /** - * Range: `>= 0 <= 1` - */ - min_speechiness?: number; - min_tempo?: number; - /** - * Range `<= 11` - */ - min_time_signature?: number; - /** - * Range: `>= 0 <= 1` - */ - min_valence?: number; - /** - * Range: `>= 0 <= 1` - */ - target_acousticness?: number; - /** - * Range: `>= 0 <= 1` - */ - target_danceability?: number; - /** - * Target duration of the track (ms) - */ - target_duration_ms?: number; - /** - * Range: `>= 0 <= 1` - */ - target_energy?: number; - /** - * Range: `>= 0 <= 1` - */ - target_instrumentalness?: number; - /** - * Range: `>= 0 <= 11` - */ - target_key?: number; - /** - * Range: `>= 0 <= 1` - */ - target_liveness?: number; - target_loudness?: number; - /** - * Range: `>= 0 <= 1` - */ - target_mode?: number; - /** - * Range `>= 0 <= 100` - */ - target_popularity?: number; - target_speechiness?: number; - /** - * Target tempo (BPM) - */ - target_tempo?: number; - target_time_signature?: number; - /** - * Range: `>= 0 <= 1` - */ - target_valence?: number; -} - -export interface RecommendationSeed extends JSONObject { - /** - * The number of tracks available after min_* and max_* filters have been applied. - */ - afterFilteringSize: number; - /** - * The number of tracks available after relinking for regional availability. - */ - afterRelinkingSize: number; - /** - * A link to the full track or artist data for this seed. - * - * For tracks this will be a link to a Track Object. \ - * For artists a link to an Artist Object. \ - * For genre seeds, this value will be null. - */ - href: string | null; - /** - * The id used to select this seed. This will be the same as the string used in the `seed_artists`, `seed_tracks` or `seed_genres` parameter. - */ - id: string; - /** - * The number of recommended tracks available for this seed. - */ - initialPoolSize: number; - /** - * The entity type of this seed. - */ - type: "artist" | "track" | "genre"; -} - -export interface Recomendations extends JSONObject { - /** - * An array of recommendation seed objects. - */ - seeds: RecommendationSeed[]; - /** - * An array of track object (simplified) ordered according to the parameters supplied. - */ - tracks: TrackSimplified[]; -} diff --git a/src/api/user/index.ts b/src/api/user/index.ts deleted file mode 100644 index ac2a553..0000000 --- a/src/api/user/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -export * from "./user.types"; -export { - type GetFollowedArtistsOpts, - type GetUserTopItemsOpts, - type UserTopItem, - type UserTopItemType, - checkIfUserFollowsArtist, - checkIfUserFollowsArtists, - checkIfUserFollowsPlaylist, - checkIfUserFollowsUser, - checkIfUserFollowsUsers, - checkIfUsersFollowPlaylist, - followArtist, - followArtists, - followPlaylist, - followUser, - followUsers, - getCurrentUser, - getFollowedArtists, - getUser, - getUserTopArtists, - getUserTopItems, - getUserTopTracks, - unfollowArtist, - unfollowArtists, - unfollowPlaylist, - unfollowUser, - unfollowUsers -} from "./user.endpoints"; diff --git a/src/api/user/user.endpoints.ts b/src/api/user/user.endpoints.ts deleted file mode 100644 index 8fe4210..0000000 --- a/src/api/user/user.endpoints.ts +++ /dev/null @@ -1,417 +0,0 @@ -import { Artist } from "../artist/artist.types"; -import { Track } from "../track/track.types"; -import { UserPrivate, UserPublic } from "../user/user.types"; -import { - CursorPagingObject, - PagingObject, - PagingOptions -} from "../general.types"; -import { SearchParams } from "../../shared"; -import { HTTPClient } from "../client"; - -/** - * Get detailed profile information about the current user. - * - * @param client Spotify HTTPClient - */ -export const getCurrentUser = async (client: HTTPClient) => { - return await client.fetch("/me", "json"); -}; - -export type GetUserTopItemsOpts = { - /** - * Over what time frame the affinities are computed. - * - * "long_term" => calculated from several years of data \ - * "medium_term" => approximately last 6 months) \ - * "short_term" => approximately last 4 weeks - * - * @default "medium_term" - */ - time_range?: "long_term" | "medium_term" | "short_term"; -} & PagingOptions; - -export type UserTopItemType = "artists" | "tracks"; -export type UserTopItem = Artist | Track; -interface UserTopItemMap extends Record { - artists: Artist; - tracks: Track; -} - -/** - * Get the current user's top artists or tracks - * based on calculated affinity. - * - * @requires `user-top-read` - * - * @param client Spotify HTTPClient - * @param type The type of entity to return. ("artists" or "tracks") - * @param opts Additional option for request - */ -export const getUserTopItems = async ( - client: HTTPClient, - type: T, - opts?: GetUserTopItemsOpts -) => { - return await client.fetch>( - "/me/top/" + type, - "json", - { - query: opts - } - ); -}; - -export type UserTopItemsEndpoint = { - getUserTopItems: ( - type: T, - opts?: GetUserTopItemsOpts - ) => Promise>; -}; - -/** - * Get the current user's top artists based on calculated affinity. - * - * @requires `user-top-read` - * - * @param client Spotify HTTPClient - * @param opts Additional option for request - */ -export const getUserTopArtists = async ( - client: HTTPClient, - opts: GetUserTopItemsOpts -) => { - return await getUserTopItems(client, "artists", opts); -}; - -/** - * Get the current user's top tracks based on calculated affinity. - * - * @requires `user-top-read` - * - * @param client Spotify HTTPClient - * @param opts Additional option for request - */ -export const getUserTopTracks = async ( - client: HTTPClient, - opts: GetUserTopItemsOpts -) => { - return await getUserTopItems(client, "tracks", opts); -}; - -/** - * Get public profile information about a Spotify user. - * - * @param client Spotify HTTPClient - * @param user_id Spotify user ID - */ -export const getUser = async (client: HTTPClient, user_id: string) => { - return await client.fetch("/users/" + user_id, "json"); -}; - -/** - * Add the current user as a follower of a playlist. - * - * @requires `playlist-modify-public` or `playlist-modify-private` - * - * @param client Spotify HTTPClient - * @param playlist_id Spotify playlist ID - * @param is_public If true the playlist will be included in user's public - * playlists, if false it will remain private. By default - true - */ -export const followPlaylist = async ( - client: HTTPClient, - playlist_id: string, - is_public?: boolean -) => { - await client.fetch(`/playlists/${playlist_id}/followers`, "void", { - method: "PUT", - json: { public: is_public } - }); -}; - -/** - * Remove the current user as a follower of a playlist. - * - * @requires `playlist-modify-public` or `playlist-modify-private` - * - * @param client Spotify HTTPClient - * @param playlist_id Spotify playlist ID - */ -export const unfollowPlaylist = async ( - client: HTTPClient, - playlist_id: string -) => { - await client.fetch(`/playlists/${playlist_id}/followers`, "void", { - method: "DELETE" - }); -}; - -export interface GetFollowedArtistsOpts - extends SearchParams, - Pick { - /** - * The last artist ID retrieved from the previous request. - */ - after?: string; -} - -/** - * Get the current user's followed artists. - * - * @requires `user-follow-read` - * - * @param client Spotify HTTPClient - * @param opts Additional option for request - */ -export const getFollowedArtists = async ( - client: HTTPClient, - opts?: GetFollowedArtistsOpts -) => { - return ( - await client.fetch<{ artists: CursorPagingObject }>( - "/me/following", - "json", - { - query: { - ...opts, - type: "artist" - } - } - ) - ).artists; -}; - -/** - * Add the current user as a follower of one or more artists. - * - * @requires `user-follow-modify` - * - * @param client Spotify HTTPClient - * @param artist_ids List of Spotify artist IDs. Maximum 50 - */ -export const followArtists = async ( - client: HTTPClient, - artist_ids: string[] -) => { - await client.fetch("/me/following", "void", { - method: "PUT", - query: { - type: "artist", - ids: artist_ids - } - }); -}; - -/** - * Add the current user as a follower of an artist. - * - * @requires `user-follow-modify` - * - * @param client Spotify HTTPClient - * @param artist_id Spotify artist ID - */ -export const followArtist = async (client: HTTPClient, artist_id: string) => { - return await followArtists(client, [artist_id]); -}; - -/** - * Add the current user as a follower of one or more Spotify users. - * - * @requires `user-follow-modify` - * - * @param client Spotify HTTPClient - * @param artist_ids List of Spotify user IDs. Maximum 50 - */ -export const followUsers = async (client: HTTPClient, user_ids: string[]) => { - await client.fetch("/me/following", "void", { - method: "PUT", - query: { - type: "user", - ids: user_ids - } - }); -}; - -/** - * Add the current user as a follower of an user. - * - * @requires `user-follow-modify` - * - * @param client Spotify HTTPClient - * @param artist_id Spotify user ID - */ -export const followUser = async (client: HTTPClient, user_id: string) => { - return await followUsers(client, [user_id]); -}; - -/** - * Remove the current user as a follower of one or more artists. - * - * @requires `user-follow-modify` - * - * @param client Spotify HTTPClient - * @param artist_ids List of Spotify artist IDs. Maximum 50 - */ -export const unfollowArtists = async ( - client: HTTPClient, - artist_ids: string[] -) => { - await client.fetch("/me/following", "void", { - method: "DELETE", - query: { - type: "artist", - ids: artist_ids - } - }); -}; - -/** - * Remove the current user as a follower of specified artist. - * - * @requires `user-follow-modify` - * - * @param client Spotify HTTPClient - * @param artist_id Spotify artist ID - */ -export const unfollowArtist = async (client: HTTPClient, artist_id: string) => { - await unfollowArtists(client, [artist_id]); -}; - -/** - * Remove the current user as a follower of one or more Spotify users. - * - * @requires `user-follow-modify` - * - * @param client Spotify HTTPClient - * @param user_ids List of Spotify user IDs. Maximum 50 - */ -export const unfollowUsers = async (client: HTTPClient, user_ids: string[]) => { - await client.fetch("/me/following", "void", { - method: "DELETE", - query: { - type: "user", - ids: user_ids - } - }); -}; - -/** - * Remove the current user as a follower of specified Spotify user. - * - * @requires `user-follow-modify` - * - * @param client Spotify HTTPClient - * @param artist_id Spotify user ID - */ -export const unfollowUser = async (client: HTTPClient, user_id: string) => { - await unfollowUsers(client, [user_id]); -}; - -/** - * Check to see if the current user is following one or more artists. - * - * @requires `user-follow-read` - * - * @param client Spotify HTTPClient - * @param artist_ids List of Spotify artist IDs. Maximum 50 - */ -export const checkIfUserFollowsArtists = async ( - client: HTTPClient, - artist_ids: string[] -) => { - return await client.fetch("/me/following/contains", "json", { - query: { - type: "artist", - ids: artist_ids - } - }); -}; - -/** - * Check to see if the current user is following artist. - * - * @requires `user-follow-read` - * - * @param client Spotify HTTPClient - * @param artist_id Spotify artist ID - */ -export const checkIfUserFollowsArtist = async ( - client: HTTPClient, - artist_id: string -) => { - return (await checkIfUserFollowsArtists(client, [artist_id]))[0]; -}; - -/** - * Check to see if the current user is following one or more Spotify users. - * - * @requires `user-follow-read` - * - * @param client Spotify HTTPClient - * @param user_ids List of Spotify user IDs. Maximum 50 - */ -export const checkIfUserFollowsUsers = async ( - client: HTTPClient, - user_ids: string[] -) => { - return await client.fetch("/me/following/contains", "json", { - query: { - type: "user", - ids: user_ids - } - }); -}; - -/** - * Check to see if the current user is following artist. - * - * @requires `user-follow-read` - * - * @param client Spotify HTTPClient - * @param user_id Spotify user ID - */ -export const checkIfUserFollowsUser = async ( - client: HTTPClient, - user_id: string -) => { - return (await checkIfUserFollowsUsers(client, [user_id]))[0]; -}; - -/** - * Check to see if one or more Spotify users are following a specified playlist. - * - * @param client Spotify HTTPClient - * @param user_ids List of Spotify user IDs. Maximum: 5 ids. - * @param playlist_id Spotify palylist ID - */ -export const checkIfUsersFollowPlaylist = async ( - client: HTTPClient, - user_ids: string[], - playlist_id: string -) => { - return await client.fetch( - `/playlists/${playlist_id}/followers/contains`, - "json", - { - query: { - ids: user_ids - } - } - ); -}; - -/** - * Check to see Spotify user is following a specified playlist. - * - * @param client Spotify HTTPClient - * @param user_id Spotify user ID - * @param playlist_id Spotify palylist ID - */ -export const checkIfUserFollowsPlaylist = async ( - client: HTTPClient, - user_id: string, - playlist_id: string -) => { - return (await checkIfUsersFollowPlaylist(client, [user_id], playlist_id))[0]; -}; diff --git a/src/api/user/user.types.ts b/src/api/user/user.types.ts deleted file mode 100644 index e2872f6..0000000 --- a/src/api/user/user.types.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { - type ExternalUrls, - type Followers, - type Image -} from "../general.types"; -import { JSONObject } from "../../shared"; - -/** - * The spotify api object containing details of a user's public details. - */ -export interface UserPublic extends JSONObject { - /** - * The name displayed on the user's profile. - * `null` if not available - */ - display_name: string | null; - /** - * Known external URLs for this user. - */ - external_urls: ExternalUrls; - /** - * Information about the followers of the user. - */ - followers?: Followers; - /** - * A link to the Web API endpoint for this user. - */ - href: string; - /** - * The Spotify user ID for the user. - */ - id: string; - /** - * The object type: "user" - */ - type: "user"; - /** - * The user's profile image. - */ - images?: Image[]; - /** - * The Spotify URI for the user. - */ - uri: string; -} - -/** - * The product type in the User object. - */ -export type UserProductType = "free" | "open" | "premium"; - -/** - * The spotify api object containing the information of explicit content. - */ -export interface ExplicitContentSettings extends JSONObject { - /** - * When true, indicates that explicit content should not be played. - */ - filter_enabled: boolean; - /** - * When true, indicates that the explicit content setting is locked - * and can't be changed by the user. - */ - filter_locked: boolean; -} - -/** - * The spotify api object containing details of a user's public and private details. - * - * For complete information, you might consider including scopes: `user-read-private`, `user-read-email`. - */ -export interface UserPrivate extends UserPublic { - /** - * The country of the user, as set in the user's account profile. - * An ISO 3166-1 alpha-2 country code. - * - * @requires `user-read-private` - */ - country?: string; - /** - * The user's email address, as entered by the user when creating - * their account. - * - * _Important_! _This email address is unverified_; - * there is no proof that it actually belongs to the user. - * - * @requires `user-read-email` - */ - email?: string; - /** - * The user's explicit content settings. - * - * @requires `user-read-private` - */ - explicit_content?: ExplicitContentSettings; - /** - * The user's Spotify subscription level: "premium", "free", etc. - * (The subscription level "open" can be considered the same as "free".) - * - * @requires `user-read-private` - */ - product?: UserProductType; - /** - * The user's profile image. - */ - images: Image[]; - /** - * Information about the followers of the user. - */ - followers: Followers; -} diff --git a/src/artist/artist.endpoints.ts b/src/artist/artist.endpoints.ts new file mode 100644 index 0000000..1b3f1cc --- /dev/null +++ b/src/artist/artist.endpoints.ts @@ -0,0 +1,96 @@ +import type { HTTPClient } from "../client.ts"; +import type { Prettify } from "../shared.ts"; +import type { PagingObject, PagingOptions } from "../general.types.ts"; +import type { AlbumGroup, SimplifiedAlbum } from "../album/album.types.ts"; +import type { Track } from "../track/track.types.ts"; +import type { Artist } from "../artist/artist.types.ts"; + +/** + * Get Spotify catalog information for a single artist identified by their unique Spotify ID. + * + * @param client Spotify HTTPClient + * @param artistId Spotify artist ID + */ +export const getArtist = async (client: HTTPClient, artistId: string) => { + const res = await client.fetch("/v1/artists/" + artistId); + return res.json() as Promise; +}; + +/** + * Get Spotify catalog information for several artists based on their Spotify IDs. + * + * @param client Spotify HTTPClient + * @param artistIds List of the Spotify IDs for the artists. Maximum: 50 IDs. + */ +export const getSeveralArtists = async ( + client: HTTPClient, + artistIds: string[], +) => { + const res = await client.fetch("/v1/artists", { query: { ids: artistIds } }); + return (await res.json() as { artists: Artist[] }).artists; +}; + +export type GetArtistAlbumsOpts = Prettify< + PagingOptions & { + /** + * List of keywords that will be used to filter the response. + * If not supplied, all album types will be returned. + */ + include_groups?: AlbumGroup[]; + /** + * An ISO 3166-1 alpha-2 country code. + * If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + } +>; + +/** + * Get Spotify catalog information about an artist's albums. + * + * @param client Spotify HTTPClient + * @param artistId Spotify artist ID + */ +export const getArtistAlbums = async ( + client: HTTPClient, + artistId: string, + options?: GetArtistAlbumsOpts, +) => { + const res = await client.fetch(`/v1/artists/${artistId}/albums`, { + query: options, + }); + return res.json() as Promise>; +}; + +/** + * Get Spotify catalog information about an artist's top tracks by country. + * + * @param client Spotify HTTPClient + * @param artistId Spotify artist ID + * @param market An ISO 3166-1 alpha-2 country code. + */ +export const getArtistTopTracks = async ( + client: HTTPClient, + artistId: string, + market?: string, +) => { + const res = await client.fetch(`/v1/artists/${artistId}/top-tracks`, { + query: { market }, + }); + return (await res.json() as { tracks: Track[] }).tracks; +}; + +/** + * Get Spotify catalog information about artists similar to a given artist. + * Similarity is based on analysis of the Spotify community's listening history. + * + * @param client Spotify HTTPClient + * @param artistId Spotify artist ID + */ +export const getArtistRelatedArtists = async ( + client: HTTPClient, + artistId: string, +) => { + const res = await client.fetch(`/v1/artists/${artistId}/related-artists`); + return (await res.json() as { artists: Artist[] }).artists; +}; diff --git a/src/artist/artist.schemas.ts b/src/artist/artist.schemas.ts new file mode 100644 index 0000000..7f831ef --- /dev/null +++ b/src/artist/artist.schemas.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; +import { + externalUrlsSchema, + followersSchema, + imageSchema, +} from "../general.shemas.ts"; + +export const simplifiedArtistSchema = z.object({ + external_urls: externalUrlsSchema, + href: z.string().url(), + id: z.string(), + name: z.string(), + type: z.literal("artist"), + uri: z.string(), +}).strict(); + +export const artistSchema = z.object({ + followers: followersSchema, + genres: z.array(z.string()), + images: z.array(imageSchema), + popularity: z.number().min(0).max(100), +}).merge(simplifiedArtistSchema).strict(); diff --git a/src/artist/artist.test.ts b/src/artist/artist.test.ts new file mode 100644 index 0000000..f5558c9 --- /dev/null +++ b/src/artist/artist.test.ts @@ -0,0 +1,49 @@ +import { + getArtist, + getArtistAlbums, + getArtistTopTracks, + getSeveralArtists, +} from "./artist.endpoints.ts"; +import { client } from "../test_client.ts"; +import { artistSchema } from "./artist.schemas.ts"; +import { z } from "zod"; +import { pagingObjectSchema } from "../general.shemas.ts"; +import { trackSchema } from "../track/track.schemas.ts"; +import { simplifiedAlbumSchema } from "../album/album.base.schemas.ts"; + +const MOCK_ARTIST_IDS = [ + "0k17h0D3J5VfsdmQ1iZtE9", // Pink Floyd + "4Z8W4fKeB5YxbusRsdQVPb", // Radiohead + "1Ffb6ejR6Fe5IamqA5oRUF", // Bring Me The Horizon + "3RNrq3jvMZxD9ZyoOZbQOD", // Korn + "3YQKmKGau1PzlVlkL1iodx", // Twenty One Pilots +]; + +const getRandomArtistId = () => { + const randomIndex = Math.floor(Math.random() * MOCK_ARTIST_IDS.length); + return MOCK_ARTIST_IDS[randomIndex]; +}; + +Deno.test("getArtist", async () => { + const artist = await getArtist(client, getRandomArtistId()); + artistSchema.parse(artist); +}); + +Deno.test("getSeveralArtists", async () => { + const artists = await getSeveralArtists(client, MOCK_ARTIST_IDS); + z.array(artistSchema).parse(artists); +}); + +Deno.test("getArtistAlbums", async () => { + const albumsPage = await getArtistAlbums(client, getRandomArtistId()); + pagingObjectSchema(simplifiedAlbumSchema).parse(albumsPage); +}); + +Deno.test("getArtistTopTracks", async () => { + const artistTopTracks = await getArtistTopTracks( + client, + getRandomArtistId(), + "from_token", + ); + z.array(trackSchema).parse(artistTopTracks); +}); diff --git a/src/artist/artist.types.ts b/src/artist/artist.types.ts new file mode 100644 index 0000000..53b03bd --- /dev/null +++ b/src/artist/artist.types.ts @@ -0,0 +1,50 @@ +import type { ExternalUrls, Followers, Image } from "../general.types.ts"; + +export interface SimplifiedArtist { + /** + * Known external URLs for this artist. + */ + external_urls: ExternalUrls; + /** + * A link to the Web API endpoint providing full details of the artist. + */ + href: string; + /** + * The Spotify ID for the artist. + */ + id: string; + /** + * The name of the artist. + */ + name: string; + /** + * The object type. + */ + type: "artist"; + /** + * The Spotify URI for the artist. + */ + uri: string; +} + +export interface Artist extends SimplifiedArtist { + /** + * Information about the followers of the artist. + */ + followers: Followers; + /** + * A list of the genres the artist is associated with. + * If not yet classified, the array is empty. + */ + genres: string[]; + /** + * Images of the artist in various sizes, widest first. + */ + images: Image[]; + /** + * The popularity of the artist. + * The value will be between 0 and 100, with 100 being the most popular. + * The artist's popularity is calculated from the popularity of all the artist's tracks. + */ + popularity: number; +} diff --git a/src/audiobook/audiobook.endpoints.ts b/src/audiobook/audiobook.endpoints.ts new file mode 100644 index 0000000..4301bed --- /dev/null +++ b/src/audiobook/audiobook.endpoints.ts @@ -0,0 +1,192 @@ +import type { SimplifiedChapter } from "../chapter/chapter.types.ts"; +import type { HTTPClient } from "../client.ts"; +import type { Prettify } from "../shared.ts"; +import type { PagingObject, PagingOptions } from "../general.types.ts"; +import type { Audiobook, SimplifiedAudiobook } from "./audiobook.types.ts"; + +/** + * Get Spotify catalog information for a single Audiobook. + * + * @param client Spotify HTTPClient + * @param audiobookId The Spotify ID of the Audiobook + * @param market An ISO 3166-1 alpha-2 country code + */ +export const getAudiobook = async ( + client: HTTPClient, + audiobookId: string, + market?: string, +) => { + const res = await client.fetch("/v1/audiobooks/" + audiobookId, { + query: { market }, + }); + return res.json() as Promise; +}; + +/** + * Get Spotify catalog information for multiple audiobooks identified by their Spotify IDs. + * + * @param client Spotify HTTPClient + * @param audiobookIds List of the Spotify IDs for the audiobooks. Maximum: 20 IDs + * @param market An ISO 3166-1 alpha-2 country code + */ +export const getAudiobooks = async ( + client: HTTPClient, + audiobookIds: string[], + market?: string, +) => { + const res = await client.fetch("/v1/audiobooks", { + query: { market, ids: audiobookIds }, + }); + return (await res.json() as { audiobooks: Audiobook[] }).audiobooks; +}; + +export type GetAudiobookChapterOpts = Prettify< + PagingOptions & { + /** + * An ISO 3166-1 alpha-2 country code. + * If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + } +>; + +/** + * Get Spotify catalog information about an audiobook’s Chapters. + * Optional parameters can be used to limit the number of Chapters returned. + * + * @param client Spotify HTTPClient + * @param audiobookId The Spotify ID of the audiobook + * @param options Additional option for request + */ +export const getAudiobookChapters = async ( + client: HTTPClient, + audiobookId: string, + options?: GetAudiobookChapterOpts, +) => { + const res = await client.fetch(`/v1/audiobooks/${audiobookId}/chapters`, { + query: options, + }); + return res.json() as Promise>; +}; + +export type GetSavedAudiobooksOpts = Prettify< + PagingOptions & { + /** + * An ISO 3166-1 alpha-2 country code. + * If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + } +>; + +/** + * Get a list of the audiobooks saved in the current Spotify user's 'Your Audiobooks' library. + * + * @param client Spotify HTTPClient + * @param options Additional option for request + */ +export const getSavedAudiobooks = async ( + client: HTTPClient, + options?: GetSavedAudiobooksOpts, +) => { + const res = await client.fetch("/v1/me/audiobooks", { query: options }); + return res.json() as Promise< + PagingObject<{ + /** + * The date and time the audiobook was saved Timestamps are returned in ISO 8601 format as Coordinated Universal Time (UTC) with a zero offset: YYYY-MM-DDTHH:MM:SSZ. + */ + added_at: string; + /** + * Information about the audiobook. + */ + audiobook: SimplifiedAudiobook; + }> + >; +}; + +/** + * Save one or more audiobooks to the current user's 'Your Audiobooks' library. + * + * @param client Spotify HTTPClient + * @param audiobookIds List of the Spotify IDs for the audiobooks. Maximum: 20 IDs + */ +export const saveAudiobooks = ( + client: HTTPClient, + audiobookIds: string[], +) => { + return client.fetch("/v1/me/audiobooks", { + method: "PUT", + query: { ids: audiobookIds }, + }); +}; + +/** + * Save audiobook to the current user's 'Your Audiobooks' library. + * + * @param client Spotify HTTPClient + * @param audiobooks_id The Spotify ID of the audiobook + */ +export const saveAudiobook = ( + client: HTTPClient, + audiobookId: string, +) => { + return saveAudiobooks(client, [audiobookId]); +}; + +/** + * Remove one or more audiobooks from the current user's 'Your Audiobooks' library. + * + * @param client Spotify HTTPClient + * @param audiobookIds List of the Spotify IDs for the audiobooks. Maximum: 20 IDs + */ +export const removeSavedAudiobooks = ( + client: HTTPClient, + audiobookIds: string[], +) => { + return client.fetch("/v1/me/audiobooks", { + method: "DELETE", + query: { ids: audiobookIds }, + }); +}; + +/** + * Remove audiobook from the current user's 'Your Audiobooks' library. + * + * @param client Spotify HTTPClient + * @param audiobookId The Spotify ID of the audiobook + */ +export const removeSavedAudiobook = ( + client: HTTPClient, + audiobookId: string, +) => { + return removeSavedAudiobooks(client, [audiobookId]); +}; + +/** + * Check if one or more audiobooks is already saved in the current Spotify user's 'Your Audiobooks' library. + * + * @param client Spotify HTTPClient + * @param audiobookIds List of the Spotify IDs for the audiobooks. Maximum: 20 IDs + */ +export const checkIfAudiobooksSaved = async ( + client: HTTPClient, + audiobookIds: string[], +) => { + const res = await client.fetch("/v1/me/audiobooks/contains", { + query: { ids: audiobookIds }, + }); + return res.json() as Promise; +}; + +/** + * Check if audiobook is already saved in the current Spotify user's 'Your Audiobooks' library. + * + * @param client Spotify HTTPClient + * @param audiobookId The Spotify ID of the Audiobook + */ +export const checkIfAudiobookSaved = async ( + client: HTTPClient, + audiobookId: string, +) => { + return (await checkIfAudiobooksSaved(client, [audiobookId]))[0]!; +}; diff --git a/src/audiobook/audiobook.types.ts b/src/audiobook/audiobook.types.ts new file mode 100644 index 0000000..bad5ea6 --- /dev/null +++ b/src/audiobook/audiobook.types.ts @@ -0,0 +1,92 @@ +import type { SimplifiedChapter } from "../chapter/chapter.types.ts"; +import type { + Author, + Copyright, + ExternalUrls, + Image, + Narrator, + PagingObject, +} from "../general.types.ts"; + +export interface SimplifiedAudiobook { + /** + * The author(s) for the audiobook. + */ + authors: Author[]; + /** + * A list of the countries in which the audiobook can be played. + */ + available_markets: string[]; + /** + * The copyright statements of the audiobook. + */ + copyrights: Copyright[]; + /** + * The description of the audiobook without html tags. + */ + description: string; + /** + * The description of the audiobook with html tags. + */ + html_description: string; + /** + * The edition of the audiobook. + */ + edition: string; + /** + * Whether or not the audiobook has explicit lyrics. + */ + explicit: boolean; + /** + * External URLs for this audiobook. + */ + external_urls: ExternalUrls; + /** + * A link to the Web API endpoint providing full details of the audiobook. + */ + href: string; + /** + * The Spotify ID for the audiobook. + */ + id: string; + /** + * Images of the audiobook in various sizes, widest first. + */ + images: Image[]; + /** + * A list of the languages used in the audiobook, identified by their ISO 639-1 code. + */ + languages: string[]; + /** + * The media type of the audiobook. + */ + media_type: string; + /** + * The name of the audiobook. + */ + name: string; + /** + * The narrator(s) for the audiobook. + */ + narrators: Narrator[]; + /** + * The publisher of the audiobook. + */ + publisher: string; + type: "audiobook"; + /** + * The Spotify URI for the audiobook. + */ + uri: string; + /** + * The number of chapters in this audiobook. + */ + total_chapters: number; +} + +export interface Audiobook extends SimplifiedAudiobook { + /** + * The chapters of the audiobook. + */ + chapters: PagingObject; +} diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..44106d3 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,81 @@ +export const SPOTIFY_AUTH_URL = "https://accounts.spotify.com/"; + +export const OAUTH_SCOPES = { + /** + * Write access to user-provided images. + */ + UGC_IMAGE_UPLOAD: "ugc-image-upload", + /** + * Read access to a user’s player state. + */ + USER_READ_PLAYBACK_STATE: "user-read-playback-state", + /** + * Write access to a user’s playback state. + */ + USER_MODIFY_PLAYBACK_STATE: "user-modify-playback-state", + /** + * Read access to a user’s currently playing content. + */ + USER_READ_CURRENTLY_PLAYING: "user-read-currently-playing", + /** + * Control playback of a Spotify track. + * + * !The user must have a `Spotify Premium` account. + */ + STREAMING: "streaming", + /** + * Read access to user's private playlists. + */ + PLAYLIST_READ_PRIVATE: "playlist-read-private", + /** + * Include collaborative playlists when requesting a user's playlists. + */ + PLAYLIST_READ_COLLABORATIVE: "playlist-read-collaborative", + /** + * Write access to a user's private playlists. + */ + PLAYLIST_MODIFY_PRIVATE: "playlist-modify-private", + /** + * Write access to a user's public playlists. + */ + PLAYLIST_MODIFY_PUBLIC: "playlist-modify-public", + /** + * Write/delete access to the list of artists and other users + * that the user follows. + */ + USER_FOLLOW_MODIFY: "user-follow-modify", + /** + * Read access to the list of artists and other users that the user follows. + */ + USER_FOLLOW_READ: "user-follow-read", + /** + * Read access to a user’s playback position in a content. + */ + USER_READ_PLAYBACK_POSITION: "user-read-playback-position", + /** + * Read access to a user's top artists and tracks. + */ + USER_TOP_READ: "user-top-read", + /** + * Read access to a user’s recently played tracks. + */ + USER_READ_RECENTLY_PLAYED: "user-read-recently-played", + /** + * Write/delete access to a user's "Your Music" library. + */ + USER_LIBRARY_MODIFY: "user-library-modify", + /** + * Read access to a user's library. + */ + USER_LIBRARY_READ: "user-library-read", + /** + * Read access to user’s email address. + */ + USER_READ_EMAIL: "user-read-email", + /** + * Read access to user’s subscription details (type of user account). + */ + USER_READ_PRIVATE: "user-read-private", +} as const; + +export type OAuthScope = (typeof OAUTH_SCOPES)[keyof typeof OAUTH_SCOPES]; diff --git a/src/auth/__mocks__.ts b/src/auth/__mocks__.ts deleted file mode 100644 index 98bd619..0000000 --- a/src/auth/__mocks__.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { faker } from "@faker-js/faker"; -import { - AuthScope, - KeypairResponse, - SCOPES, - ScopedAccessResponse -} from "./general"; - -export const getRandomScopes = (): AuthScope[] => { - return faker.helpers.arrayElements(Object.values(SCOPES)); -}; - -export const getKeypairResponse = (): KeypairResponse => { - return { - refresh_token: faker.string.alphanumeric(64), - access_token: faker.string.alphanumeric(64), - expires_in: 3600, - token_type: "Bearer", - scope: getRandomScopes().join(" ") - }; -}; - -export const getScopedResponse = (): ScopedAccessResponse => { - return { - access_token: faker.string.alphanumeric(64), - expires_in: 3600, - token_type: "Bearer", - scope: getRandomScopes().join(" ") - }; -}; diff --git a/src/auth/auth_code.test.ts b/src/auth/auth_code.test.ts deleted file mode 100644 index 12c0283..0000000 --- a/src/auth/auth_code.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { AuthCodeFlow } from "./auth_code"; -import createFetchMock from "vitest-fetch-mock"; -import { AuthError, AuthErrorObject, getBasicAuthHeader } from "./general"; -import { faker } from "@faker-js/faker"; -import { - getKeypairResponse, - getRandomScopes, - getScopedResponse -} from "./__mocks__"; -import { afterEach, beforeEach, expect } from "vitest"; -import { vi, it } from "vitest"; - -const fetchMocker = createFetchMock(vi); -fetchMocker.enableMocks(); - -let authFlow: AuthCodeFlow; -let creds: ConstructorParameters[0]; - -beforeEach(() => { - creds = { - client_id: faker.string.alphanumeric(32), - client_secret: faker.string.alphanumeric(86) - }; - - authFlow = new AuthCodeFlow(creds); -}); - -afterEach(() => { - fetchMocker.mockClear(); - vi.clearAllMocks(); -}); - -it("AuthCode: constructor", () => { - expect(authFlow["basicAuthHeader"]).toBe( - getBasicAuthHeader(creds.client_id, creds.client_secret) - ); -}); - -it("AuthCode: getAuthURL", () => { - const opts = { - scopes: getRandomScopes(), - redirect_uri: faker.internet.url(), - show_dialog: faker.datatype.boolean(), - state: faker.string.uuid() - }; - - const url = authFlow.getAuthURL(opts); - - expect(Object.fromEntries(url.searchParams)).toMatchObject({ - client_id: creds.client_id, - scope: opts.scopes.join(" "), - redirect_uri: opts.redirect_uri, - response_type: "code", - show_dialog: opts.show_dialog.toString(), - state: opts.state - }); -}); - -it("AuthCode: getGrantData #1", async () => { - const redirect_uri = faker.internet.url(); - const code = faker.string.alphanumeric(48); - const mockResponse = getKeypairResponse(); - - fetchMocker.doMockOnce((req) => { - const url = new URL(req.url); - expect(req.method).toBe("POST"); - expect(url.pathname).toBe("/api/token"); - - const basicToken = req.headers.get("Authorization"); - expect(basicToken).toBe( - getBasicAuthHeader(creds.client_id, creds.client_secret) - ); - - expect(Object.fromEntries(url.searchParams)).toMatchObject({ - redirect_uri, - code, - grant_type: "authorization_code" - }); - - return { body: JSON.stringify(mockResponse) }; - }); - - const result = await authFlow.getGrantData(redirect_uri, code); - - expect(result).toMatchObject(mockResponse); -}); - -it("AuthCode: getGrantData #2", async () => { - const rawError: AuthErrorObject = { - error: "invalid_client", - error_description: "Something went wront" - }; - - fetchMocker.doMockOnce((req) => { - const url = new URL(req.url); - expect(req.method).toBe("POST"); - expect(url.pathname).toBe("/api/token"); - - return { body: JSON.stringify(rawError), status: 500 }; - }); - - try { - await authFlow.getGrantData( - faker.internet.url(), - faker.string.alphanumeric(48) - ); - } catch (error) { - expect(error).toBeInstanceOf(AuthError); - if (!(error instanceof AuthError)) return; - expect(error.status).toBe(500); - expect(error.message).toBe("invalid_client"); - expect(error.raw).toMatchObject(rawError); - } -}); - -it("AuthCode: refresh #1", async () => { - const refresh_token = faker.string.alphanumeric(64); - const mockResponse = getScopedResponse(); - - fetchMocker.doMockOnce((req) => { - const url = new URL(req.url); - expect(req.method).toBe("POST"); - expect(url.pathname).toBe("/api/token"); - - const basicToken = req.headers.get("Authorization"); - expect(basicToken).toBe( - getBasicAuthHeader(creds.client_id, creds.client_secret) - ); - - expect(Object.fromEntries(url.searchParams)).toMatchObject({ - refresh_token, - grant_type: "refresh_token" - }); - - return { body: JSON.stringify(mockResponse) }; - }); - - const data = await authFlow.refresh(refresh_token); - - expect(data).toMatchObject(mockResponse); -}); - -it("AuthCode: refresh #2", async () => { - const refresh_token = faker.string.alphanumeric(64); - const rawError: AuthErrorObject = { - error: faker.lorem.lines(1), - error_description: faker.lorem.lines(1) - }; - - fetchMocker.doMockOnce((req) => { - const url = new URL(req.url); - expect(req.method).toBe("POST"); - expect(url.pathname).toBe("/api/token"); - - return { body: JSON.stringify(rawError), status: 500 }; - }); - - try { - await authFlow.refresh(refresh_token); - } catch (error) { - expect(error).toBeInstanceOf(AuthError); - if (!(error instanceof AuthError)) return; - expect(error.status).toBe(500); - expect(error.message).toBe(rawError.error); - expect(error.raw).toMatchObject(rawError); - } -}); - -// it("AuthCode: createRefresher", async () => { -// const refresh_token = faker.random.alphaNumeric(64); -// const access_token = faker.random.alphaNumeric(64); -// const mockResponse = getScopedResponse(); - -// const refreshSpy = vi -// .spyOn(authFlow, "refresh") -// .mockResolvedValueOnce(mockResponse); -// const onRefreshSuccess = vi.fn(); - -// const refresher = authFlow.createRefresher(refresh_token); - -// expect(authProvider.token).toBe(access_token); - -// await authProvider.refresher(); - -// expect(refreshSpy).toBeCalledWith(refresh_token); -// expect(refreshSpy).toBeCalledTimes(1); -// expect(onRefreshSuccess).toBeCalledWith(mockResponse); -// }); diff --git a/src/auth/auth_code.ts b/src/auth/auth_code.ts deleted file mode 100644 index b33c505..0000000 --- a/src/auth/auth_code.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { IAuthProvider, parseResponse, toQueryString } from "../shared"; -import { - ApiTokenReqParams, - AuthorizeReqParams, - AuthScope, - getBasicAuthHeader, - KeypairResponse, - parseCallbackData, - ScopedAccessResponse, - SPOTIFY_AUTH, - AuthError, - URL_ENCODED, - Refresher, - AuthErrorObject -} from "./general"; - -type GetAuthURLOpts = { - /** - * URI for redirection after the user grants or denies permission. Must be included in "Redirect URIs" in the Spotify app settings. - */ - redirect_uri: string; - /** - * The 'state' parameter is an optional string included in the authorization URL to protect against CSRF attacks. - * - * It is highly recommended to use. - * Can be any random string, for example generated by `crypto.randomUUID()`. - */ - state?: string; - /** - * List of scopes. - * - * If no scopes are specified, authorization will be granted - * only to access publicly available information. - */ - scopes?: AuthScope[]; - /** - * Whether or not to force the user to approve the app again - * if they’ve already done so. - * - * - If false, a user who has already approved the application may be automatically redirected to the URI specified by `redirect_uri`. - * - If true, the user will not be automatically redirected and will have to approve the app again. - * - * @default false - */ - show_dialog?: boolean; -}; - -/** - * Authorization Code Flow - */ -export class AuthCodeFlow { - private readonly basicAuthHeader: string; - - /** - * @param creds Spotify application credentials required for Authorization Code flow. - */ - constructor( - private readonly creds: { - /** - * The Client ID generated after registering your Spotify application. - */ - client_id: string; - /** - * The Client Secret generated after registering your Spotify application. - */ - client_secret: string; - } - ) { - this.basicAuthHeader = getBasicAuthHeader( - creds.client_id, - creds.client_secret - ); - } - - /** - * Creates a URL to redirect the user to the Spotify authorization page, where they can grant or deny permission to your app. - * - * @param opts The object of options that will be passed as search parameters in the authorization URL. - * @returns An instance of a URL object that can be converted to a string by calling the `.toString()` method on it. - * - * @example - * ```ts - * const authURL = authFlow.getAuthURL({ - * redirect_uri: "YOUR_REDIRECT_URI", - * scopes: ["user-read-email"], - * state: "123abc", - * }); - * ``` - */ - getAuthURL({ scopes, ...opts }: GetAuthURLOpts): URL { - const url = new URL(SPOTIFY_AUTH + "authorize"); - - url.search = toQueryString({ - response_type: "code", - scope: scopes?.join(" "), - client_id: this.creds.client_id, - ...opts - }); - - return url; - } - - /** - * Helper function that parses the callback data returned by an spotify authorization server in search params. - */ - static parseCallbackData = parseCallbackData; - - /** - * Retrieves an access and refresh token from the Spotify API using an authorization code and client credentials. - * - * @param redirect_uri URI for redirection after the user grants or denies permission. - * @param code An authorization code that you received from callback query params. - * - * @returns Spotify response containing access and refresh token. The following example, shows how the successful response looks like: - * ```json - * { - * "access_token": "NgCXRK...MzYjw", - * "token_type": "Bearer", - * "scope": "user-read-private user-read-email", - * "expires_in": 3600, - * "refresh_token": "NgAagA...Um_SHo" - * } - * ``` - */ - async getGrantData(redirect_uri: string, code: string) { - const url = new URL(SPOTIFY_AUTH + "api/token"); - url.search = toQueryString({ - code, - redirect_uri, - grant_type: "authorization_code" - }); - - const res = await fetch(url, { - method: "POST", - headers: { - Authorization: this.basicAuthHeader, - "Content-Type": URL_ENCODED - } - }); - - if (!res.ok) { - throw new AuthError( - await parseResponse(res), - res.status - ); - } - - return (await res.json()) as KeypairResponse; - } - - /** - * Requests a new access token using your refresh token and client credentials - */ - async refresh(refresh_token: string) { - const url = new URL(SPOTIFY_AUTH + "api/token"); - url.search = toQueryString({ - refresh_token, - grant_type: "refresh_token" - }); - - const res = await fetch(url, { - method: "POST", - headers: { - Authorization: this.basicAuthHeader, - "Content-Type": URL_ENCODED - } - }); - - if (!res.ok) { - throw new AuthError( - await parseResponse(res), - res.status - ); - } - - return (await res.json()) as ScopedAccessResponse; - } - - createRefresher(refresh_token: string): Refresher { - return this.refresh.bind(this, refresh_token); - } - - createAuthProvider( - refresh_token: string, - access_token?: string - ): IAuthProvider { - const refresher = this.createRefresher(refresh_token); - return { - refresh: async () => (await refresher()).access_token, - token: access_token - }; - } -} diff --git a/src/auth/client_credentials.ts b/src/auth/client_credentials.ts deleted file mode 100644 index ecd3cc1..0000000 --- a/src/auth/client_credentials.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { IAuthProvider, parseResponse } from "../shared"; -import { - AccessResponse, - getBasicAuthHeader, - SPOTIFY_AUTH, - AuthError, - URL_ENCODED, - AuthErrorObject -} from "./general"; - -/** - * Client Credentials Flow - */ -export class ClientCredentialsFlow { - private readonly basicAuthHeader: string; - - constructor(creds: { client_id: string; client_secret: string }) { - this.basicAuthHeader = getBasicAuthHeader( - creds.client_id, - creds.client_secret - ); - } - - async getAccessToken() { - const res = await fetch(SPOTIFY_AUTH + "api/token", { - method: "POST", - headers: { - Authorization: this.basicAuthHeader, - "Content-Type": URL_ENCODED - }, - body: new URLSearchParams({ - grant_type: "client_credentials" - }) - }); - - if (!res.ok) { - throw new AuthError( - await parseResponse(res), - res.status - ); - } - - return (await res.json()) as AccessResponse; - } - - createRefresher() { - return this.getAccessToken.bind(this); - } - - createAuthProvider(access_token?: string): IAuthProvider { - const refresher = this.createRefresher(); - return { - refresh: async () => (await refresher()).access_token, - token: access_token - }; - } -} diff --git a/src/auth/general.test.ts b/src/auth/general.test.ts deleted file mode 100644 index 61270c4..0000000 --- a/src/auth/general.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - AuthCodeCallbackData, - AuthError, - getBasicAuthHeader, - parseCallbackData -} from "./general"; -import { faker } from "@faker-js/faker"; -import { vi, it, expect } from "vitest"; - -it("parseCallbackData with code", () => { - const callbackData: AuthCodeCallbackData = { - code: faker.string.alphanumeric(128), - state: faker.string.alphanumeric(64) - }; - - const result = parseCallbackData(new URLSearchParams(callbackData)); - - expect(result).toMatchObject(callbackData); -}); - -it("parseCallbackData with error", () => { - const callbackData: AuthCodeCallbackData = { - error: faker.lorem.lines(), - state: faker.string.alphanumeric(64) - }; - - const result = parseCallbackData(new URLSearchParams(callbackData)); - - expect(result).toMatchObject(callbackData); -}); - -it("parseCallbackData without state", () => { - const callbackData: AuthCodeCallbackData = { - code: faker.string.alphanumeric(128) - }; - - const result = parseCallbackData(new URLSearchParams(callbackData)); - - expect(result).toMatchObject(callbackData); -}); - -it("parseCallbackData with invalid data", () => { - expect(() => parseCallbackData(new URLSearchParams())).toThrowError( - "Invalid params" - ); -}); - -it("getBasicAuthHeader", () => { - expect(getBasicAuthHeader("123", "456")).toBe("Basic MTIzOjQ1Ng=="); -}); diff --git a/src/auth/general.ts b/src/auth/general.ts deleted file mode 100644 index 7187903..0000000 --- a/src/auth/general.ts +++ /dev/null @@ -1,234 +0,0 @@ -export const SPOTIFY_AUTH = "https://accounts.spotify.com/"; -export const URL_ENCODED = "application/x-www-form-urlencoded;"; - -/** - * Scopes provide Spotify users using third-party apps the confidence that only - * the information they choose to share will be shared, and nothing more. - */ -export const SCOPES = { - // Images ------------------------------------------------------------------- - /** - * Write access to user-provided images. - */ - UGC_IMAGE_UPLOAD: "ugc-image-upload", - - // Spotify connect ---------------------------------------------------------- - /** - * Read access to a user’s player state. - */ - USER_READ_PLAYBACK_STATE: "user-read-playback-state", - /** - * Write access to a user’s playback state. - */ - USER_MODIFY_PLAYBACK_STATE: "user-modify-playback-state", - /** - * Read access to a user’s currently playing content. - */ - USER_READ_CURRENTLY_PLAYING: "user-read-currently-playing", - - // Playback ----------------------------------------------------------------- - /** - * Control playback of a Spotify track. - * - * !The user must have a `Spotify Premium` account. - */ - STREAMING: "streaming", - - // Playlist ----------------------------------------------------------------- - /** - * Read access to user's private playlists. - */ - PLAYLIST_READ_PRIVATE: "playlist-read-private", - /** - * Include collaborative playlists when requesting a user's playlists. - */ - PLAYLIST_READ_COLLABORATIVE: "playlist-read-collaborative", - /** - * Write access to a user's private playlists. - */ - PLAYLIST_MODIFY_PRIVATE: "playlist-modify-private", - /** - * Write access to a user's public playlists. - */ - PLAYLIST_MODIFY_PUBLIC: "playlist-modify-public", - - // Follow ------------------------------------------------------------------- - /** - * Write/delete access to the list of artists and other users - * that the user follows. - */ - USER_FOLLOW_MODIFY: "user-follow-modify", - /** - * Read access to the list of artists and other users that the user follows. - */ - USER_FOLLOW_READ: "user-follow-read", - - // Listening History -------------------------------------------------------- - /** - * Read access to a user’s playback position in a content. - */ - USER_READ_PLAYBACK_POSITION: "user-read-playback-position", - /** - * Read access to a user's top artists and tracks. - */ - USER_TOP_READ: "user-top-read", - /** - * Read access to a user’s recently played tracks. - */ - USER_READ_RECENTLY_PLAYED: "user-read-recently-played", - - // Library ------------------------------------------------------------------ - /** - * Write/delete access to a user's "Your Music" library. - */ - USER_LIBRARY_MODIFY: "user-library-modify", - /** - * Read access to a user's library. - */ - USER_LIBRARY_READ: "user-library-read", - // Users - /** - * Read access to user’s email address. - */ - USER_READ_EMAIL: "user-read-email", - /** - * Read access to user’s subscription details (type of user account). - */ - USER_READ_PRIVATE: "user-read-private" -} as const; - -export type AuthScope = (typeof SCOPES)[keyof typeof SCOPES]; - -/** - * @see https://developer.spotify.com/documentation/web-api/concepts/api-calls#authentication-error-object - */ -export type AuthErrorObject = { - error: string; - error_description?: string; -}; - -export class AuthError extends Error { - constructor( - public readonly raw: string | AuthErrorObject, - public readonly status: number, - options?: ErrorOptions - ) { - super(typeof raw === "object" ? raw.error : raw, options); - this.name = "AuthError"; - } -} - -export type AuthCodeCallbackSuccess = { - /** - * An authorization code that can be exchanged for an Access Token. - */ - code: string; - /** - * The value of the state parameter supplied in the request - */ - state?: string; -}; - -export type AuthCodeCallbackError = { - /** - * The reason authorization failed, for example: “access_denied” - */ - error: string; - /** - * The value of the state parameter supplied in the request - */ - state?: string; -}; - -export type AuthCodeCallbackData = - | AuthCodeCallbackSuccess - | AuthCodeCallbackError; - -export const parseCallbackData = (searchParams: URLSearchParams) => { - const params = Object.fromEntries( - searchParams - ) as Partial; - - if ("code" in params || "error" in params) { - return params as AuthCodeCallbackData; - } - - throw new Error("Invalid params"); -}; - -export const getBasicAuthHeader = ( - client_id: string, - client_secret: string -) => { - return ( - "Basic " + - (__IS_NODE__ - ? Buffer.from(client_id + ":" + client_secret).toString("base64") - : btoa(client_id + ":" + client_secret)) - ); -}; - -export interface AccessResponse { - /** - * An Access Token that can be provided in subsequent calls, - * for example to Spotify Web API services. - */ - access_token: string; - /** - * How the Access Token may be used: always “Bearer”. - */ - token_type: "Bearer"; - /** - * The time period (in seconds) for which the Access Token is valid. - */ - expires_in: number; -} - -export interface ScopedAccessResponse extends AccessResponse { - /** - * A space-separated list of scopes which have been granted - * for this `access_token` - */ - scope?: string; -} - -/** - * Spotify response data containing refresh and access tokens - */ -export interface KeypairResponse extends ScopedAccessResponse { - /** - * A token that can be used to generate new `access_token`. - */ - refresh_token: string; -} - -/** - * Search parameters for the `GET` request to the `/authorize` endpoint - */ -export type AuthorizeReqParams = { - client_id: string; - response_type: "code" | "token"; - redirect_uri: string; - state?: string; - scope?: string; - show_dialog?: boolean; - code_challenge_method?: "S256"; - code_challenge?: string; -}; - -/** - * Search parameters for the `GET` request to the `/api/token` endpoint - */ -export type ApiTokenReqParams = { - refresh_token?: string; - code?: string; - redirect_uri?: string; - client_id?: string; - code_verifier?: string; - grant_type: "refresh_token" | "client_credentials" | "authorization_code"; -}; - -type ResponseWithToken = { access_token: string }; - -export type Refresher = - () => Promise; diff --git a/src/auth/implicit_grant.ts b/src/auth/implicit_grant.ts deleted file mode 100644 index d891569..0000000 --- a/src/auth/implicit_grant.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { toQueryString } from "../shared"; -import { AuthorizeReqParams, AuthScope, SPOTIFY_AUTH } from "./general"; - -export type GetAuthURLOpts = { - /** - * The URI to redirect to after the user grants or denies permission. - */ - redirect_uri: string; - /** - * List of scopes. - * - * @default - * If no scopes are specified, authorization will be granted - * only to access publicly available information - */ - scopes?: AuthScope[]; - /** - * Whether or not to force the user to approve the app again - * if they’ve already done so. - * - * - If false, a user who has already approved the application - * may be automatically redirected to the URI specified by `redirect_uri`. - * - If true, the user will not be automatically redirected and will have - * to approve the app again. - * - * @default false - */ - show_dialog?: boolean; - /** - * This provides protection against attacks such as - * cross-site request forgery. - */ - state?: string; -}; - -export interface CallbackErrorData extends Record { - /** - * The reason authorization failed, for example: “access_denied”. - */ - error: string; - /** - * The value of the state parameter supplied in the request. - */ - state?: string; -} - -export interface CallbackSuccessData - extends Record { - /** - * An access token that can be provided in subsequent calls, for example to Spotify Web API services. - */ - access_token: string; - token_type: "Bearer"; - /** - * The time period (in seconds) for which the access token is valid. - */ - expires_in: string; - /** - * The value of the state parameter supplied in the request. - */ - state?: string; -} - -export type CallbackData = CallbackSuccessData | CallbackErrorData; - -/** - * Implicit Grant Flow - */ -export class ImplicitFlow { - constructor(private readonly client_id: string) {} - - getAuthURL({ scopes, ...opts }: GetAuthURLOpts) { - const url = new URL(SPOTIFY_AUTH + "authorize"); - - url.search = toQueryString({ - response_type: "token", - scope: scopes?.join(" "), - client_id: this.client_id, - ...opts - }); - - return url; - } - - static parseCallbackData(hash: string) { - const params = Object.fromEntries( - new URLSearchParams(hash.substring(1)) - ) as CallbackData; - - if ("error" in params) return params; - if ( - "access_token" in params && - "expires_in" in params && - "token_type" in params - ) { - return params; - } - - throw new Error("Invalid params"); - } -} diff --git a/src/auth/index.ts b/src/auth/index.ts deleted file mode 100644 index a9c08fe..0000000 --- a/src/auth/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export { - type AccessResponse, - type AuthCodeCallbackData, - type AuthCodeCallbackError, - type AuthCodeCallbackSuccess, - type AuthScope, - getBasicAuthHeader, - type KeypairResponse, - type ScopedAccessResponse, - SCOPES, - AuthError -} from "./general"; - -export { AuthCodeFlow } from "./auth_code"; -export { PKCECodeFlow } from "./pkce_auth_code"; -export { ClientCredentialsFlow } from "./client_credentials"; -export { ImplicitFlow } from "./implicit_grant"; diff --git a/src/auth/pkce_auth_code.ts b/src/auth/pkce_auth_code.ts deleted file mode 100644 index 14c61a3..0000000 --- a/src/auth/pkce_auth_code.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { IAuthProvider, parseResponse, toQueryString } from "../shared"; -import { - ApiTokenReqParams, - AuthorizeReqParams, - AuthScope, - KeypairResponse, - parseCallbackData, - SPOTIFY_AUTH, - AuthError, - URL_ENCODED, - AuthErrorObject -} from "./general"; - -export type GetAuthURLOpts = { - /** - * The URI to redirect to after the user grants or denies permission. - */ - redirect_uri: string; - /** - * PKCE code that you generated from `code_verifier`. - * You can get it with the `getCodeChallenge` function. - */ - code_challenge: string; - /** - * This provides protection against attacks such as - * cross-site request forgery. - */ - state?: string; - /** - * List of scopes. - * - * @default - * If no scopes are specified, authorization will be granted - * only to access publicly available information - */ - scopes?: AuthScope[]; -}; - -export type GetGrantDataOpts = { - /** - * The URI to redirect to after the user grants or denies permission. - */ - redirect_uri: string; - /** - * The random code you generated before redirecting the user to spotify auth - */ - code_verifier: string; - /** - * An authorization code that can be exchanged for an Access Token. - * The code that Spotify produces after redirecting to `redirect_uri`. - */ - code: string; -}; - -const VERIFIER_CHARS = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; - -/** - * Authorization Code with PKCE Flow - */ -export class PKCECodeFlow { - constructor(private readonly client_id: string) {} - - getAuthURL({ scopes, ...opts }: GetAuthURLOpts) { - const url = new URL(SPOTIFY_AUTH + "authorize"); - - url.search = toQueryString({ - response_type: "code", - scope: scopes?.join(" "), - code_challenge_method: "S256", - client_id: this.client_id, - ...opts - }); - - return url; - } - - /** - * Generates random PKCE Code Verifier - * - * The code verifier is a random string between 43 and 128 characters in length. - * - * @param length Must be between 43 and 128 characters - * @default 64 - */ - static async generateCodeVerifier(length = 64) { - const randomBytes = __IS_NODE__ - ? new Uint8Array((await import("node:crypto")).randomBytes(length)) - : crypto.getRandomValues(new Uint8Array(length)); - - let codeVerifier = ""; - for (let i = 0; i < length; i++) { - codeVerifier += VERIFIER_CHARS[randomBytes[i]! % VERIFIER_CHARS.length]; - } - - return codeVerifier; - } - - static async getCodeChallenge(code_verifier: string) { - if (__IS_NODE__) { - return (await import("node:crypto")) - .createHash("sha256") - .update(code_verifier) - .digest("base64url"); - } - - const buffer = await crypto.subtle.digest( - "SHA-256", - new TextEncoder().encode(code_verifier) - ); - - return btoa(String.fromCharCode(...new Uint8Array(buffer))) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/, ""); - } - - /** - * Shorthand for generating PKCE codes. - * Uses `generateCodeVerifier` and `getCodeChallenge` under the hood. - */ - static async generateCodes(verifierLength?: number) { - const code_verifier = await this.generateCodeVerifier(verifierLength); - const code_challenge = await this.getCodeChallenge(code_verifier); - - return { code_verifier, code_challenge }; - } - - static parseCallbackData = parseCallbackData; - - async getGrantData(opts: GetGrantDataOpts) { - const url = new URL(SPOTIFY_AUTH + "api/token"); - url.search = toQueryString({ - grant_type: "authorization_code", - client_id: this.client_id, - ...opts - }); - - const res = await fetch(url, { - headers: { - "Content-Type": URL_ENCODED - }, - method: "POST" - }); - - if (!res.ok) { - throw new AuthError( - await parseResponse(res), - res.status - ); - } - - return (await res.json()) as KeypairResponse; - } - - /** - * Requests a new token keypair using your old refresh token and client ID - */ - async refresh(refresh_token: string) { - const url = new URL(SPOTIFY_AUTH + "api/token"); - url.search = toQueryString({ - grant_type: "refresh_token", - client_id: this.client_id, - refresh_token - }); - - const res = await fetch(url, { - headers: { - "Content-Type": URL_ENCODED - }, - method: "POST" - }); - - if (!res.ok) { - throw new AuthError( - await parseResponse(res), - res.status - ); - } - - return (await res.json()) as KeypairResponse; - } - - createRefresher(refresh_token: string) { - return (async () => { - const data = await this.refresh(refresh_token); - refresh_token = data.refresh_token; - return data; - }).bind(this); - } - - createAuthProvider( - refresh_token: string, - access_token?: string - ): IAuthProvider { - const refresher = this.createRefresher(refresh_token); - return { - refresh: async () => (await refresher()).access_token, - token: access_token - }; - } -} diff --git a/src/category/category.endpoints.ts b/src/category/category.endpoints.ts new file mode 100644 index 0000000..eb5d64b --- /dev/null +++ b/src/category/category.endpoints.ts @@ -0,0 +1,81 @@ +import type { PagingObject } from "../general.types.ts"; +import type { Category } from "./category.types.ts"; +import type { HTTPClient } from "../client.ts"; + +export type GetBrowseCategoriesOpts = { + /** + * An ISO 3166-1 alpha-2 country code. + * If a country code is specified, only content that is available in that market will be returned. + */ + country?: string; + /** + * The maximum number of items to return. Minimum: 1. Maximum: 50. + * @default 20 + */ + limit?: number; + /** + * The desired language, consisting of an ISO 639-1 language code and an ISO 3166-1 alpha-2 country code, joined by an underscore. + * + * Provide this parameter if you want the category metadata returned in a particular language. + * + * @example "es_MX" - meaning "Spanish (Mexico)". + */ + locale?: string; + /** + * The index of the first item to return. + * Use with limit to get the next set of items. + * + * @default 0 + */ + offset?: number; +}; + +/** + * Get a list of categories used to tag items in Spotify + * (on, for example, the Spotify player’s “Browse” tab). + * + * @param client Spotify HTTPClient + * @param options Additional option for request + */ +export const getBrowseCategories = async ( + client: HTTPClient, + options?: GetBrowseCategoriesOpts, +) => { + const res = await client.fetch("/v1/browse/categories", { + query: options, + }); + return (await res.json() as { categories: PagingObject }) + .categories; +}; + +export type GetBrowseCategoryOpts = { + /** + * An ISO 3166-1 alpha-2 country code. + * Provide this parameter to ensure that the category exists for a particular country. + */ + country?: string; + /** + * The maximum number of items to return. Minimum: 1. Maximum: 50. + * @default 20 + */ + limit?: number; +}; + +/** + * Get a single category used to tag items in Spotify + * (on, for example, the Spotify player’s “Browse” tab). + * + * @param client Spotify HTTPClient + * @param categoryId The Spotify category ID for the category + * @param options Additional option for request + */ +export const getBrowseCategory = async ( + client: HTTPClient, + categoryId: string, + options?: GetBrowseCategoryOpts, +) => { + const res = await client.fetch("/v1/browse/categories/" + categoryId, { + query: options, + }); + return res.json() as Promise; +}; diff --git a/src/category/category.types.ts b/src/category/category.types.ts new file mode 100644 index 0000000..234a4ac --- /dev/null +++ b/src/category/category.types.ts @@ -0,0 +1,21 @@ +import type { NonNullableObject } from "../shared.ts"; +import type { Image } from "../general.types.ts"; + +export type Category = { + /** + * A link to the Web API endpoint returning full details of the category. + */ + href: string; + /** + * The category icon, in various sizes. + */ + icons: NonNullableObject[]; + /** + * The Spotify category ID of the category. + */ + id: string; + /** + * The name of the category. + */ + name: string; +}; diff --git a/src/chapter/chapter.endpoints.ts b/src/chapter/chapter.endpoints.ts new file mode 100644 index 0000000..70788fd --- /dev/null +++ b/src/chapter/chapter.endpoints.ts @@ -0,0 +1,35 @@ +import type { HTTPClient } from "../client.ts"; +import type { Chapter } from "./chapter.types.ts"; + +/** + * @param client Spotify HTTPClient + * @param chapterId The Spotify ID of the chapter + * @param market An ISO 3166-1 alpha-2 country code + */ +export const getChapter = async ( + client: HTTPClient, + chapterId: string, + market?: string, +) => { + const res = await client.fetch("/v1/chapters/" + chapterId, { + query: { market }, + }); + return res.json() as Promise; +}; + +/** + * @param client Spotify HTTPClient + * @param chapterIds List of the Spotify IDs of the chapters. Maximum: 20 IDs + * @param market An ISO 3166-1 alpha-2 country code + * @returns + */ +export const getChapters = async ( + client: HTTPClient, + chapterIds: string[], + market?: string, +) => { + const res = await client.fetch("/v1/chapters", { + query: { market, ids: chapterIds }, + }); + return (await res.json() as { chapters: Chapter[] }).chapters; +}; diff --git a/src/chapter/chapter.types.ts b/src/chapter/chapter.types.ts new file mode 100644 index 0000000..67f8f89 --- /dev/null +++ b/src/chapter/chapter.types.ts @@ -0,0 +1,97 @@ +import type { SimplifiedAudiobook } from "../audiobook/audiobook.types.ts"; +import type { + ExternalUrls, + Image, + ReleaseDatePrecision, + Restrictions, + ResumePoint, +} from "../general.types.ts"; + +export interface SimplifiedChapter { + /** + * A URL to a 30 second preview (MP3 format). + */ + audio_preview_url: string; + /** + * A list of the countries in which the episode can be played. + */ + available_markets: string[]; + /** + * The number of the episode + */ + chapter_number: number; + /** + * The description of the episode without html tags. + */ + description: string; + /** + * The description of the episode with html tags. + */ + html_description: string; + /** + * The episode length in milliseconds. + */ + duration_ms: number; + /** + * Whether or not the episode has explicit lyrics. + */ + explicit: boolean; + /** + * External URLs for this episode. + */ + external_urls: ExternalUrls; + /** + * A link to the Web API endpoint providing full details of the episode. + */ + href: string; + /** + * The Spotify ID for the episode. + */ + id: string; + /** + * Images of the episode in various sizes, widest first. + */ + images: Image[]; + /** + * If true, the episode is playable in the given market. + * Otherwise false. + */ + is_playable: boolean; + /** + * A list of the languages used in the episode, identified by their ISO 639-1 code. + */ + languages: string[]; + /** + * The name of the episode. + */ + name: string; + /** + * The date the episode was first released. + * Depending on the precision it might be shown in different ways + */ + release_date: string; + /** + * The precision with which `release_date` value is known. + */ + release_date_precision: ReleaseDatePrecision; + /** + * The user's most recent position in the episode. + */ + resume_point: ResumePoint; + /** + * The object type. + */ + type: "episode"; + /** + * The Spotify URI for the episode. + */ + uri: string; + /** + * Included in the response when a content restriction is applied. + */ + restrictions?: Restrictions; +} + +export interface Chapter extends SimplifiedChapter { + audiobook: SimplifiedAudiobook; +} diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..f4ebc2d --- /dev/null +++ b/src/client.ts @@ -0,0 +1,199 @@ +import type { SearchParams } from "./shared.ts"; + +/** + * @see https://developer.spotify.com/documentation/web-api/concepts/api-calls#regular-error-object + */ +export type RegularErrorObject = { + error: { + message: string; + status: number; + reason?: string; + }; +}; + +export class SpotifyError extends Error { + name = "SpotifyError"; + + public readonly response: Response; + public readonly body: RegularErrorObject | string | null; + + constructor( + message: string, + response: Response, + body: RegularErrorObject | string | null, + options?: ErrorOptions, + ) { + super(message, options); + this.response = response; + this.body = body; + } + + get url() { + return this.response.url; + } + + get status() { + return this.response.status; + } +} + +const APP_JSON = "application/json"; +const CONTENT_TYPE = "Content-Type"; + +const createSpotifyError = async ( + response: Response, + options?: ErrorOptions, +) => { + const urlWithoutQuery = response.url.split("?")[0]; + let message = response.statusText + ? `${response.status} ${response.statusText} (${urlWithoutQuery})` + : `${response.status} (${urlWithoutQuery})`; + let body: RegularErrorObject | string | null = null; + + if (response.body && response.type !== "opaque") { + try { + body = await response.text(); + const contentType = response.headers.get(CONTENT_TYPE); + + if ( + contentType && + (contentType === APP_JSON || contentType.split(";")[0] === APP_JSON) + ) { + body = JSON.parse(body); + } + } catch (_) { + /* Ignore errors */ + } + } + + const bodyMessage = body === null + ? null + : typeof body === "string" + ? body + : body.error.message; + if (bodyMessage) { + message += " : " + bodyMessage; + } + + return new SpotifyError(message, response, body, options); +}; + +export interface FetchLikeOptions extends Omit { + query?: SearchParams; + body?: BodyInit | null | Record | unknown[]; +} + +type FetchLike = ( + resource: URL, + options: FetchLikeOptions, +) => Promise; +export type Middleware = (next: FetchLike) => FetchLike; + +/** + * Interface that provides a fetch method to make HTTP requests to Spotify API. + */ +export interface HTTPClient { + fetch(path: string, options?: FetchLikeOptions): Promise; +} + +const isPlainObject = (obj: unknown): obj is Record => { + return ( + typeof obj === "object" && + obj !== null && + Object.prototype.toString.call(obj) === "[object Object]" + ); +}; + +export type SpotifyClinetOptions = { + fetch?: (input: URL, init?: RequestInit) => Promise; + baseUrl?: string; + /** + * @returns new access token + */ + refresher?: () => Promise; + /** + * @default false + */ + waitForRateLimit?: boolean | ((retryAfter: number) => boolean); + middlewares?: Middleware[]; +}; + +export class SpotifyClient implements HTTPClient { + private readonly baseUrl: string; + + constructor( + private accessToken: string, + private readonly options: SpotifyClinetOptions = {}, + ) { + this.baseUrl = options.baseUrl + ? options.baseUrl + : "https://api.spotify.com/"; + } + + fetch(path: string, opts: FetchLikeOptions = {}) { + const url = new URL(path, this.baseUrl); + if (opts.query) { + for (const key in opts.query) { + const value = opts.query[key]; + if (typeof value !== "undefined") { + url.searchParams.set(key, value.toString()); + } + } + } + const headers = new Headers(opts.headers); + headers.set("Accept", APP_JSON); + + const isBodyJSON = !!opts.body && + (isPlainObject(opts.body) || Array.isArray(opts.body)); + if (isBodyJSON) { + headers.set(CONTENT_TYPE, APP_JSON); + } + + const body = isBodyJSON + ? JSON.stringify(opts.body) + : (opts.body as BodyInit | null | undefined); + + let isRefreshed = false; + + const recursiveFetch = async (): Promise => { + headers.set("Authorization", "Bearer " + this.accessToken); + + const res = await (this.options.middlewares || []).reduceRight( + (next, mw) => mw(next), + (this.options.fetch || globalThis.fetch) as FetchLike, + )(url, { ...opts, body, headers }); + + if (res.ok) return res; + + if (res.status === 401 && this.options.refresher && !isRefreshed) { + this.accessToken = await this.options.refresher(); + isRefreshed = true; + return recursiveFetch(); + } + + if (res.status === 429) { + // time in seconds + const retryAfter = Number(res.headers.get("Retry-After")) || undefined; + + if (retryAfter) { + const waitForRateLimit = + typeof this.options.waitForRateLimit === "function" + ? this.options.waitForRateLimit(retryAfter) + : this.options.waitForRateLimit; + + if (waitForRateLimit) { + await new Promise((resolve) => + setTimeout(resolve, retryAfter * 1000) + ); + } + + return recursiveFetch(); + } + } + + throw await createSpotifyError(res); + }; + + return recursiveFetch(); + } +} diff --git a/src/episode/episode.endpoints.ts b/src/episode/episode.endpoints.ts new file mode 100644 index 0000000..a428e3a --- /dev/null +++ b/src/episode/episode.endpoints.ts @@ -0,0 +1,158 @@ +import type { HTTPClient } from "../client.ts"; +import type { Episode } from "./episode.types.ts"; +import type { PagingObject, PagingOptions } from "../general.types.ts"; +import type { Prettify } from "../shared.ts"; + +/** + * Get Spotify catalog informnation for a single episode. + * + * @param client Spotify HTTPClient + * @param episodeId The Spotify ID of the episode + * @param market An ISO 3166-1 alpha-2 country code + */ +export const getEpisode = async ( + client: HTTPClient, + episodeId: string, + market?: string, +) => { + const res = await client.fetch("/v1/episodes/" + episodeId, { + query: { market }, + }); + return res.json() as Promise; +}; + +/** + * Get spotify catalog information for multiple episodes identified by their Spotify IDs. + * + * @param client Spotify HTTPClient + * @param episodeIds List of the Spotify IDs of the episodes. Maximum: 20 IDs + * @param market An ISO 3166-1 alpha-2 country code + */ +export const getEpisodes = async ( + client: HTTPClient, + episodeIds: string[], + market?: string, +) => { + const res = await client.fetch("/v1/episodes", { + query: { market, ids: episodeIds }, + }); + return (await res.json() as { episodes: Episode[] }).episodes; +}; + +export type GetSavedEpisodesOpts = Prettify< + PagingOptions & { + /** + * An ISO 3166-1 alpha-2 country code. + * If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + } +>; + +/** + * Get a list of the episodes saved in the current Spotify user's 'Your Shows' library. + * + * @param client Spotify HTTPClient + * @param options Additional option for request + */ +export const getSavedEpisodes = async ( + client: HTTPClient, + options?: GetSavedEpisodesOpts, +) => { + const res = await client.fetch("/v1/me/episodes", { query: options }); + return res.json() as Promise< + PagingObject<{ + /** + * The date and time the episode was saved Timestamps are returned in ISO 8601 format as Coordinated Universal Time (UTC) with a zero offset: YYYY-MM-DDTHH:MM:SSZ. + */ + added_at: string; + /** + * Information about the episode. + */ + episode: Episode; + }> + >; +}; + +/** + * Save one or more episodes to the current user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param episodeIds List of the Spotify IDs for the episodes. Maximum: 20 IDs + */ +export const saveEpisodes = ( + client: HTTPClient, + episodeIds: string[], +) => { + return client.fetch(`/v1/me/episodes`, { + method: "PUT", + query: { ids: episodeIds }, + }); +}; + +/** + * Save episode to the current user's 'Your Shows' library. + * + * @param client Spotify HTTPClient + * @param episodeId The Spotify ID of the episode + */ +export const saveEpisode = (client: HTTPClient, episodeId: string) => { + return saveEpisodes(client, [episodeId]); +}; + +/** + * Remove one or more episodes from the current user's 'Your Shows' library. + * + * @param client Spotify HTTPClient + * @param episodeIds List of the Spotify IDs for the episodes. Maximum: 20 IDs + */ +export const removeSavedEpisodes = ( + client: HTTPClient, + episodeIds: string[], +) => { + return client.fetch("/v1/me/episodes", { + query: { ids: episodeIds }, + }); +}; + +/** + * Remove episode from the current user's 'Your Shows' library. + * + * @param client Spotify HTTPClient + * @param episodeId List of the Spotify IDs for the episodes. Maximum: 20 IDs + */ +export const removeSavedEpisode = ( + client: HTTPClient, + episodeId: string, +) => { + return removeSavedEpisodes(client, [episodeId]); +}; + +/** + * Check if one or more episodes is already saved in the current Spotify user's 'Your Shows' library. + * + * @param client Spotify HTTPClient + * @param episodeIds List of the Spotify IDs for the episodes. Maximum: 20 IDs + */ +export const checkIfEpisodesSaved = async ( + client: HTTPClient, + episodeIds: string[], +) => { + const res = await client.fetch("/v1/me/episodes/contains", { + query: { ids: episodeIds }, + }); + return res.json() as Promise; +}; + +/** + * Check if epsisode is already saved in the current Spotify user's 'Your Shows' library. + * + * @param client Spotify HTTPClient + * @param episodeId The Spotify ID for the episode + */ +export const checkIfEpisodeSaved = async ( + client: HTTPClient, + episodeId: string, +) => { + return (await checkIfEpisodesSaved(client, [episodeId]))[0]!; +}; diff --git a/src/episode/episode.types.ts b/src/episode/episode.types.ts new file mode 100644 index 0000000..09ad869 --- /dev/null +++ b/src/episode/episode.types.ts @@ -0,0 +1,97 @@ +import type { + ExternalUrls, + Image, + ReleaseDatePrecision, + Restrictions, + ResumePoint, +} from "../general.types.ts"; +import type { SimplifiedShow } from "../show/show.types.ts"; + +export interface SimplifiedEpisode { + /** + * A URL to a 30 second preview (MP3 format). + */ + audio_preview_url: string; + /** + * A description of the episode without HTML tags. + */ + description: string; + /** + * A description of the episode with HTML tags. + */ + html_description: string; + /** + * The episode length in milliseconds. + */ + duration_ms: number; + /** + * Whether or not the episode has explicit lyrics. + */ + explicit: boolean; + /** + * External URLs for this episode. + */ + external_urls: ExternalUrls; + /** + * A link to the Web API endpoint providing full details of the episode. + */ + href: string; + /** + * The Spotify ID for the episode. + */ + id: string; + /** + * Images of the episode in various sizes, widest first. + */ + images: Image[]; + /** + * True, if the episode is hosted outside of Spotify's CDN. + */ + is_externally_hosted: boolean; + /** + * If true, the episode is playable in the given market. + * Otherwise false. + */ + is_playable: boolean; + /** + * The language used in the episode. Identified by a ISO 639 code. Deprecated. + */ + language?: string; + /** + * A list of the languages used in the episode, identified by their ISO 639-1 code. + */ + languages: string[]; + /** + * The name of the episode. + */ + name: string; + /** + * The date the episode was first released. + * Depending on the precision it might be shown in different ways + */ + release_date: string; + /** + * The precision with which `release_date` value is known. + */ + release_date_precision: ReleaseDatePrecision; + /** + * The user's most recent position in the episode. + */ + resume_point: ResumePoint; + type: "episode"; + /** + * The Spotify URI for the episode. + */ + uri: string; + /** + * Included in the response when a content restriction is applied. + */ + restrictions?: Restrictions; +} + +export interface Episode extends SimplifiedEpisode { + /** + * The show on which episode belongs. + */ + show: SimplifiedShow; +} diff --git a/src/general.shemas.ts b/src/general.shemas.ts new file mode 100644 index 0000000..e053574 --- /dev/null +++ b/src/general.shemas.ts @@ -0,0 +1,57 @@ +import { z, ZodType } from "zod"; + +export const pagingObjectSchema = (itemSchema: TItem) => + z.object({ + href: z.string().url(), + limit: z.number().positive(), + next: z.string().url().nullable(), + offset: z.number(), + previous: z.string().url().nullable(), + total: z.number().positive(), + items: z.array(itemSchema), + }).strict(); + +export const externalUrlsSchema = z.object({ + spotify: z.string().url(), +}); + +export const imageSchema = z.object({ + url: z.string().url(), + height: z.number().nullable(), + width: z.number().nullable(), +}).strict(); + +export const releaseDatePrecisionEnum = z.enum(["day", "month", "year"]); + +export const restrictionsSchema = z.object({ + reason: z.enum(["market", "product", "explicit"]), +}); + +export const externalIdsSchema = z.object({ + isrc: z.string().optional(), + ean: z.string().optional(), + upc: z.string().optional(), +}).strict(); + +export const copyrightSchema = z.object({ + text: z.string(), + type: z.enum(["C", "P"]), +}).strict(); + +export const resumePointSchema = z.object({ + fully_played: z.boolean(), + resume_position_ms: z.number().positive(), +}).strict(); + +export const followersSchema = z.object({ + href: z.string().url().nullable(), + total: z.number().positive(), +}).strict(); + +export const authorSchema = z.object({ + name: z.string(), +}).strict(); + +export const narratorSchema = z.object({ + name: z.string(), +}).strict(); diff --git a/src/general.types.ts b/src/general.types.ts new file mode 100644 index 0000000..718a24b --- /dev/null +++ b/src/general.types.ts @@ -0,0 +1,196 @@ +import { Episode } from "./episode/episode.types.ts"; +import { Track } from "./track/track.types.ts"; + +export type PagingObject = { + /** + * A link to the Web API endpoint returning the full result of the request. + */ + href: string; + /** + * The maximum number of items in the response. + */ + limit: number; + /** + * URL to the next page of items. + */ + next: string | null; + /** + * The offset of the items returned. + */ + offset: number; + /** + * URL to the previous page of items + */ + previous: string | null; + /** + * The total number of items available to return. + */ + total: number; + items: TItem[]; +}; + +export type CursorPagingObject = { + /** + * A link to the Web API endpoint returning the full result of the request. + */ + href: string; + /** + * The maximum number of items in the response. + */ + limit: number; + /** + * URL to the next page of items. + */ + next: string | null; + /** + * The cursors used to find the next set of items. + */ + cursors: { + /** + * The cursor to use as key to find the next page of items. + */ + after: string; + /** + * The cursor to use as key to find the previous page of items. + */ + before: string; + }; + /** + * The total number of items available to return. + */ + total: number; + items: TItem[]; +}; + +export type PagingOptions = { + /** + * The maximum number of items to return. Minimum: 1. Maximum: 50. + * @default 20 + */ + limit?: number; + /** + * The index of the first item to return. Use with limit to get the next set of items. + * @default 0 (the first item) + */ + offset?: number; +}; + +/** + * The reason for the restriction. + * + * "market" - The content item is not available in the given market. \ + * "product" - The content item is not available for the user's subscription type. \ + * "explicit" - The content item is explicit and the user's account is set to not play explicit content. + */ +export type RestrictionsReason = "market" | "product" | "explicit"; + +export type Restrictions = { + /** + * The reason for the restriction. + * + * Episodes may be restricted if the content is not available in a given market, to the user's subscription type, or when the user's account is set to not play explicit content. + */ + reason: RestrictionsReason; +}; + +/** + * The precision with which `release_date` value is known. + */ +export type ReleaseDatePrecision = "year" | "month" | "day"; + +export type Image = { + /** + * The image height in pixels. + */ + height: number | null; + /** + * The source URL of the image. + */ + url: string; + /** + * The image width in pixels. + */ + width: number | null; +}; + +export type ResumePoint = { + /** + * Whether or not the episode has been fully played by the user. + */ + fully_played: boolean; + /** + * The user's most recent position in the episode in milliseconds + */ + resume_position_ms: number; +}; + +export type Followers = { + /** + * This will always be set to null, as the Web API does not support it at the moment. + */ + href: string | null; + /** + * The total number of followers. + */ + total: number; +}; + +export type Author = { + /** + * The name of the author. + */ + name: string; +}; + +export type Narrator = { + /** + * The name of the narrator. + */ + name: string; +}; + +export type ExternalUrls = { + spotify: string; +}; + +export type ExternalIds = { + /** + * [International Standard Recording Code](https://en.wikipedia.org/wiki/International_Standard_Recording_Code). + */ + isrc?: string; + /** + * [International Article Number](http://en.wikipedia.org/wiki/International_Article_Number_%28EAN%29). + */ + ean?: string; + /** + * [Universal Product Code](http://en.wikipedia.org/wiki/Universal_Product_Code). + */ + upc?: string; +}; + +/** + * The copyright object contains the type and the name of copyright. + */ +export type Copyright = { + /** + * The copyright text for this content. + */ + text: string; + /** + * The type of copyright: \ + * C = the copyright \ + * P = the sound recording (performance) copyright + */ + type: "C" | "P"; +}; + +export type ItemType = + | "artist" + | "album" + | "playlist" + | "track" + | "show" + | "episode" + | "audiobook"; + +export type TrackItem = Track | Episode; diff --git a/src/genre/genre.endpoints.ts b/src/genre/genre.endpoints.ts new file mode 100644 index 0000000..76275f8 --- /dev/null +++ b/src/genre/genre.endpoints.ts @@ -0,0 +1,11 @@ +import type { HTTPClient } from "../client.ts"; + +/** + * Retrieve a list of available genres seed parameter values for recommendations. + * + * @param client Spotify HTTPClient + */ +export const getAvailableGenres = async (client: HTTPClient) => { + const res = await client.fetch("/v1/recommendations/available-genre-seeds"); + return ((await res.json()) as { genres: string[] }).genres; +}; diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index e366e44..0000000 --- a/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./api"; -export * from "./auth"; -export * from "./shared"; -export * from "./paginator"; diff --git a/src/market/market.endpoints.ts b/src/market/market.endpoints.ts new file mode 100644 index 0000000..b16a7cb --- /dev/null +++ b/src/market/market.endpoints.ts @@ -0,0 +1,11 @@ +import type { HTTPClient } from "../client.ts"; + +/** + * Get the list of markets where Spotify is available. + * + * @param client Spotify HTTPClient + */ +export const getAvailableMarkets = async (client: HTTPClient) => { + const res = await client.fetch("/v1/markets"); + return (await res.json() as { markets: string[] }).markets; +}; diff --git a/src/mod.ts b/src/mod.ts new file mode 100644 index 0000000..dd7d5c7 --- /dev/null +++ b/src/mod.ts @@ -0,0 +1,27 @@ +export * from "./auth.ts"; +export * from "./client.ts"; +export * from "./general.types.ts"; + +export * from "./user/user.types.ts"; +export * from "./user/user.endpoints.ts"; +export * from "./track/track.types.ts"; +export * from "./track/track.endpoints.ts"; +export * from "./show/show.types.ts"; +export * from "./show/show.endpoints.ts"; +export * from "./search/search.endpoints.ts"; +export * from "./playlist/playlist.types.ts"; +export * from "./playlist/playlist.endpoints.ts"; +export * from "./market/market.endpoints.ts"; +export * from "./genre/genre.endpoints.ts"; +export * from "./episode/episode.types.ts"; +export * from "./episode/episode.endpoints.ts"; +export * from "./chapter/chapter.types.ts"; +export * from "./chapter/chapter.endpoints.ts"; +export * from "./category/category.types.ts"; +export * from "./category/category.endpoints.ts"; +export * from "./audiobook/audiobook.types.ts"; +export * from "./audiobook/audiobook.endpoints.ts"; +export * from "./artist/artist.types.ts"; +export * from "./artist/artist.endpoints.ts"; +export * from "./album/album.types.ts"; +export * from "./album/album.endpoints.ts"; diff --git a/src/pagination.ts b/src/pagination.ts new file mode 100644 index 0000000..a827351 --- /dev/null +++ b/src/pagination.ts @@ -0,0 +1,116 @@ +import type { Prettify } from "./shared.ts"; +import type { PagingObject, PagingOptions } from "./general.types.ts"; + +/** + * Represents the possible directions a paginator can take, where the values of "next" and "prev" indicate whether the iterator is navigating forward or backward. + */ +type PaginatorDirection = "next" | "prev"; + +type NextPageOptions = { + limit?: number; + setOffset?: (offset: number) => number; +}; + +type PageIterOptions = Prettify< + PagingOptions & { + direction?: PaginatorDirection; + } +>; + +const DEFAULTS: Required = { + direction: "next", + limit: 20, + offset: 0, +}; + +export class ChunkIterator { + private defaults: Required; + + constructor( + private fetcher: (opts: PagingOptions) => Promise>, + defaults: PageIterOptions = {}, + ) { + this.defaults = { ...DEFAULTS, ...defaults }; + } + + asyncIterator() { + return this[Symbol.asyncIterator](); + } + + [Symbol.asyncIterator](): AsyncIterator< + TItem[], + TItem[], + NextPageOptions | undefined + > { + let done = false; + let { direction, limit, offset } = this.defaults; + + return { + next: async (opts = {}) => { + if (done) return { done, value: [] }; + limit = opts.limit ?? this.defaults.limit; + offset = opts.setOffset ? opts.setOffset(offset) : offset; + + const chunk = await this.fetcher({ limit, offset }); + + if ( + (direction === "next" && !chunk.next) || + (direction === "prev" && !chunk.previous) + ) { + done = true; + return { value: chunk.items, done: false }; + } + + offset = direction === "next" ? offset + limit : offset - limit; + return { value: chunk.items, done }; + }, + }; + } +} + +export class PageIterator { + private defaults: Required; + + constructor( + private fetcher: ( + opts: PagingOptions, + ) => Promise>, + defaults: PageIterOptions = {}, + ) { + this.defaults = { ...DEFAULTS, ...defaults }; + } + + asyncIterator() { + return this[Symbol.asyncIterator](); + } + + async *[Symbol.asyncIterator](): AsyncGenerator { + let { direction, limit, offset } = this.defaults; + + while (true) { + const chunk = await this.fetcher({ limit, offset }); + + if ( + (direction === "next" && !chunk.next) || + (direction === "prev" && !chunk.previous) + ) { + const last = chunk.items.pop()!; + for (const item of chunk.items) yield item; + + return last; + } + + for (const item of chunk.items) yield item; + + offset = direction === "next" ? offset + limit : offset - limit; + } + } + + async collect() { + const items: TItem[] = []; + for await (const item of this) { + items.push(item); + } + return items; + } +} diff --git a/src/paginator.ts b/src/paginator.ts deleted file mode 100644 index 93af782..0000000 --- a/src/paginator.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { JSONObject } from "./shared"; -import { PagingObject, PagingOptions } from "./api/general.types"; - -/** - * Represents the possible directions a paginator can take, where the values of "next" and "prev" indicate whether the iterator is navigating forward or backward. - */ -type PaginatorDirection = "next" | "prev"; - -type NextPageOpts = { - limit?: number; - setOffset?: (offset: number) => number; -}; - -type PaginatorOpts = PagingOptions & { - direction?: PaginatorDirection; -}; - -const DEFAULTS: Required = { - direction: "next", - limit: 20, - offset: 0 -}; - -export class ChunkPaginator { - private defaults: Required; - - constructor( - private fetcher: (opts: PagingOptions) => Promise>, - defaults: PaginatorOpts = {} - ) { - for (const key in DEFAULTS) { - if (!defaults[key]) defaults[key] = DEFAULTS[key]; - } - this.defaults = defaults as Required; - } - - asyncIterator() { - return this[Symbol.asyncIterator](); - } - - [Symbol.asyncIterator](): AsyncIterator { - let done = false; - let { direction, limit, offset } = this.defaults; - - return { - next: async (opts = {}) => { - if (done) return { done, value: [] }; - limit = opts.limit ?? this.defaults.limit; - offset = opts.setOffset ? opts.setOffset(offset) : offset; - - const chunk = await this.fetcher({ limit, offset }); - - if ( - (direction === "next" && !chunk.next) || - (direction === "prev" && !chunk.previous) - ) { - done = true; - return { value: chunk.items, done: false }; - } - - offset = direction === "next" ? offset + limit : offset - limit; - return { value: chunk.items, done }; - } - }; - } -} - -export class Paginator { - private defaults: Required; - - constructor( - private fetcher: (opts: PagingOptions) => Promise>, - defaults: PaginatorOpts = {} - ) { - for (const key in DEFAULTS) { - if (!defaults[key]) defaults[key] = DEFAULTS[key]; - } - this.defaults = defaults as Required; - } - - asyncIterator() { - return this[Symbol.asyncIterator](); - } - - async *[Symbol.asyncIterator](): AsyncGenerator { - let { direction, limit, offset } = this.defaults; - - while (true) { - const chunk = await this.fetcher({ limit, offset }); - - if ( - (direction === "next" && !chunk.next) || - (direction === "prev" && !chunk.previous) - ) { - const last = chunk.items.pop()!; - for (const item of chunk.items) yield item; - - return last; - } - - for (const item of chunk.items) yield item; - - offset = direction === "next" ? offset + limit : offset - limit; - } - } - - async collect() { - const items: T[] = []; - for await (const item of this) { - items.push(item); - } - return items; - } -} diff --git a/src/player/player.endpoints.ts b/src/player/player.endpoints.ts new file mode 100644 index 0000000..355ed46 --- /dev/null +++ b/src/player/player.endpoints.ts @@ -0,0 +1,350 @@ +import type { HTTPClient } from "../client.ts"; +import { CursorPagingObject } from "../general.types.ts"; +import type { + Device, + PlaybackState, + PlayHistoryObject, + RepeatMode, +} from "./player.types.ts"; +import { Queue } from "./player.types.ts"; + +export type GetPlaybackStateOpts = { + /** + * An ISO 3166-1 alpha-2 country code. If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + /** + * A list of item types that your client supports besides the default track type. + */ + additional_types?: ("track" | "episode")[]; +}; + +/** + * Get information about the user’s current playback state, including track or episode, progress, and active device. + * + * @requires `user-read-playback-state` + * + * @param client Spotify HTTPClient + * @param options Additional options for request + */ +export const getPlaybackState = async ( + client: HTTPClient, + options: GetPlaybackStateOpts = {}, +) => { + const res = await client.fetch("/v1/me/player", { + query: { + market: options.market, + additional_types: options.additional_types?.join(","), + }, + }); + if (res.status === 204) return null; + return res.json() as Promise; +}; + +export type TransferPlaybackBody = { + /** + * A JSON array containing the ID of the device on which playback should be started/transferred. + */ + device_ids: string[]; + /** + * "true" - ensure playback happens on new device. \ + * "false" - keep the current playback state. + * + * @default false + */ + play?: boolean; +}; + +/** + * Transfer playback to a new device and optionally begin playback. The order of execution is not guaranteed when you use this API with other Player API endpoints. + * + * @requires `user-modify-playback-state` + */ +export const transferPlayback = ( + client: HTTPClient, + body: TransferPlaybackBody, +) => { + return client.fetch("/v1/me/player", { method: "PUT", body }); +}; + +/** + * Get information about a user’s available Spotify Connect devices. Some device models are not supported and will not be listed in the API response. + * + * @requires `user-read-playback-state` + */ +export const getAvailableDevices = async (client: HTTPClient) => { + const res = await client.fetch("/v1/me/player/devices"); + return (await res.json() as { devices: Device[] }).devices; +}; + +export type GetCurrentPlayingTrackOpts = { + /** + * An ISO 3166-1 alpha-2 country code. If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + /** + * A list of item types that your client supports besides the default track type. + */ + additional_types?: ("track" | "episode")[]; +}; + +/** + * Get the object currently being played on the user's Spotify account. + * + * @requires `user-read-currently-playing` + */ +export const getCurrentPlayingTrack = async ( + client: HTTPClient, + options?: GetCurrentPlayingTrackOpts, +) => { + const res = await client.fetch("/v1/me/player/currently-playing", { + query: options, + }); + if (res.status === 204) return null; + return res.json() as Promise; +}; + +export type StartResumePlaybackBody = { + /** + * The id of the device this command is targeting. If not supplied, the user's currently active device is the target. + */ + device_id?: string; + /** + * The position to start playback. Must be a positive number. + */ + position_ms?: number; + /** + * Spotify URI of the context to play. Valid contexts are albums, artists & playlists. + * + * @example "spotify:album:1Je1IMUlBXcx1Fz0WE7oPT" + */ + context_uri?: string; + /** + * A JSON array of the Spotify track URIs to play. + * + * @example + * ```json + * ["spotify:track:4iV5W9uYEdYUVa79Axb7Rh", + * "spotify:track:1301WleyT98MSxVHPZCA6M"] + * ``` + */ + uris?: string[]; + /** + * Indicates from where in the context playback should start. + */ + offset?: { + /** + * The index of the first track to play. + */ + position?: number; + /** + * The track URI in the context. + */ + uri?: string; + }; +}; + +/** + * Start a new context or resume current playback on the user’s active device. + * + * @requires `user-modify-playback-state` + */ +export const startPlayback = ( + client: HTTPClient, + options: StartResumePlaybackBody = {}, +) => { + const { device_id, ...body } = options; + return client.fetch("/v1/me/player/play", { + method: "PUT", + body, + query: { device_id }, + }); +}; + +/** + * Start a new context or resume current playback on the user’s active device. + * + * @requires `user-modify-playback-state` + */ +export const resumePlayback = startPlayback; + +/** + * Pause playback on the user’s account. + * + * @requires `user-modify-playback-state` + * + * @param client Spotify HTTPClient + * @param deviceId The id of the device this command is targeting. If not supplied, the user's currently active device is the target. + */ +export const pausePlayback = ( + client: HTTPClient, + deviceId?: string, +) => { + return client.fetch("/v1/me/player/pause", { + method: "PUT", + query: { device_id: deviceId }, + }); +}; + +/** + * Skips to next track in the user’s queue. + * + * @requires `user-modify-playback-state` + * + * @param client Spotify HTTPClient + * @param deviceId The id of the device this command is targeting. If not supplied, the user's currently active device is the target. + */ +export const skipToNext = ( + client: HTTPClient, + deviceId?: string, +) => { + return client.fetch("/v1/me/player/next", { + method: "POST", + query: { device_id: deviceId }, + }); +}; + +/** + * Skips to previous track in the user’s queue. + * + * @requires `user-modify-playback-state` + * + * @param client Spotify HTTPClient + * @param deviceId The id of the device this command is targeting. If not supplied, the user's currently active device is the target. + */ +export const skipToPrevious = ( + client: HTTPClient, + deviceId?: string, +) => { + return client.fetch("/v1/me/player/previous", { + method: "POST", + query: { device_id: deviceId }, + }); +}; + +/** + * Seeks to the given position in the user’s currently playing track. + * + * @requires `user-modify-playback-state` + * + * @param client Spotify HTTPClient + * @param positionMs The position in milliseconds to seek to. Must be a positive number. Passing in a position that is greater than the length of the track will cause the player to start playing the next song. + * @param deviceId The id of the device this command is targeting. If not supplied, the user's currently active device is the target. + */ +export const seekToPosition = ( + client: HTTPClient, + positionMs: number, + deviceId?: string, +) => { + return client.fetch("/v1/me/player/seek", { + method: "PUT", + query: { position_ms: positionMs, device_id: deviceId }, + }); +}; + +/** + * Set the repeat mode for the user's playback. + * + * @requires `user-modify-playback-state` + * + * @param client Spotify HTTPClient + * @param state + * `track` - will repeat the current track. \ + * `context` - will repeat the current context. \ + * `off` - will turn repeat off. + * @param deviceId The id of the device this command is targeting. If not supplied, the user's currently active device is the target. + */ +export const setRepeatMode = ( + client: HTTPClient, + state: RepeatMode, + deviceId?: string, +) => { + return client.fetch("/v1/me/player/repeat", { + method: "PUT", + query: { state, device_id: deviceId }, + }); +}; + +/** + * Toggle shuffle on or off for user’s playback. + * + * @requires `user-modify-playback-state` + * + * @param client Spotify HTTPClient + * @param state `true` to turn shuffle on, `false` to turn it off. + * @param deviceId The id of the device this command is targeting. If not supplied, the user's currently active device is the target. + */ +export const togglePlaybackShuffle = ( + client: HTTPClient, + state: boolean, + deviceId?: string, +) => { + return client.fetch("/v1/me/player/shuffle", { + method: "PUT", + query: { state, device_id: deviceId }, + }); +}; + +export type GetRecentlyPlayedTracksOpts = { + /** + * The maximum number of items to return. Minimum: 1. Maximum: 50. + * @default 20 + */ + limit?: number; + /** + * A Unix timestamp in milliseconds. Returns all items after (but not including) this cursor position. If after is specified, before must not be specified. + * + * @example 1484811043508 + */ + after?: number; + /** + * A Unix timestamp in milliseconds. Returns all items before (but not including) this cursor position. If before is specified, after must not be specified. + */ + before?: number; +}; + +/** + * Get tracks from the current user's recently played tracks. + * + * @requires `user-read-recently-played` + */ +export const getRecentPlayedTracks = async ( + client: HTTPClient, + options?: GetRecentlyPlayedTracksOpts, +) => { + const res = await client.fetch("/v1/me/player/recently-played", { + query: options, + }); + return res.json() as Promise>; +}; + +/** + * Get the list of objects that make up the user's queue. + * + * @requires `user-read-currently-playing`, +`user-read-playback-state` + */ +export const getUserQueue = async (client: HTTPClient) => { + const res = await client.fetch("/v1/me/player/queue"); + return res.json() as Promise; +}; + +/** + * Add an item to the end of the user's current playback queue. + * + * @requires `user-modify-playback-state` + * + * @param client Spotify HTTPClient + * @param uri The uri of the item to add to the queue. Must be a track or an episode uri. + * @param deviceId The id of the device this command is targeting. If not supplied, the user's currently active device is the target. + */ +export const addItemToPlaybackQueue = ( + client: HTTPClient, + uri: string, + deviceId?: string, +) => { + return client.fetch("/v1/me/player/queue", { + method: "POST", + query: { uri, device_id: deviceId }, + }); +}; diff --git a/src/player/player.schemas.ts b/src/player/player.schemas.ts new file mode 100644 index 0000000..3dc4858 --- /dev/null +++ b/src/player/player.schemas.ts @@ -0,0 +1,60 @@ +import { z } from "zod"; +import { externalUrlsSchema } from "../general.shemas.ts"; +import { trackSchema } from "../track/track.schemas.ts"; + +export const deviceSchema = z.object({ + id: z.string().nullable(), + is_active: z.boolean(), + is_private_session: z.boolean(), + is_restricted: z.boolean(), + name: z.string(), + type: z.string(), + volume_percent: z.number().min(0).max(100).nullable(), + supports_volume: z.boolean(), +}); + +const actionsSchema = z.object({ + interrupting_playback: z.boolean().optional(), + pausing: z.boolean().optional(), + resuming: z.boolean().optional(), + seeking: z.boolean().optional(), + skipping_next: z.boolean().optional(), + skipping_prev: z.boolean().optional(), + toggling_repeat_context: z.boolean().optional(), + toggling_shuffle: z.boolean().optional(), + toggling_repeat_track: z.boolean().optional(), + transferring_playback: z.boolean().optional(), +}); + +export const contextSchema = z.object({ + type: z.string(), + href: z.string(), + external_urls: externalUrlsSchema, + uri: z.string(), +}); + +export const playbackStateSchema = z.object({ + device: deviceSchema, + repeat_state: z.enum(["off", "track", "context"]), + shuffle_state: z.boolean(), + context: contextSchema.nullable(), + timestamp: z.number(), + progress_ms: z.number().nullable(), + is_playing: z.boolean(), + item: z.any(), + currently_playing_type: z.enum(["track", "episode", "ad", "unknown"]), + actions: z.object({ + disallows: actionsSchema, + }), +}); + +export const queueSchema = z.object({ + currently_playing: z.any().nullable(), + queue: z.array(z.any()), +}); + +export const playHistoryObjectSchema = z.object({ + track: trackSchema, + played_at: z.string(), + context: contextSchema, +}); diff --git a/src/player/player.test.ts b/src/player/player.test.ts new file mode 100644 index 0000000..b7411ad --- /dev/null +++ b/src/player/player.test.ts @@ -0,0 +1,23 @@ +import { + getAvailableDevices, + // getCurrentPlayingTrack,s + getPlaybackState, +} from "./player.endpoints.ts"; +import { client } from "../test_client.ts"; +import { deviceSchema, playbackStateSchema } from "./player.schemas.ts"; +import { z } from "zod"; + +Deno.test("getPlaybackState", async () => { + const state = await getPlaybackState(client); + playbackStateSchema.parse(state); +}); + +Deno.test("getAvailableDevices", async () => { + const devices = await getAvailableDevices(client); + z.array(deviceSchema).parse(devices); +}); + +// Deno.test("getCurrentPlayingTrack", async () => { +// const state = await getCurrentPlayingTrack(client); +// playbackStateSchema.parse(state); +// }); diff --git a/src/player/player.types.ts b/src/player/player.types.ts new file mode 100644 index 0000000..50b3066 --- /dev/null +++ b/src/player/player.types.ts @@ -0,0 +1,116 @@ +import type { ExternalUrls, TrackItem } from "../general.types.ts"; +import type { Track } from "../track/track.types.ts"; + +export type Device = { + /** + * The device ID. This ID is unique and persistent to some extent. However, this is not guaranteed and any cached device_id should periodically be cleared out and refetched as necessary. + */ + id: string | null; + /** If this device is the currently active device. */ + is_active: boolean; + /** If this device is currently in a private session. */ + is_private_session: boolean; + /** + * Whether controlling this device is restricted. At present if this is "true" then no Web API commands will be accepted by this device. + */ + is_restricted: boolean; + /** + * A human-readable name for the device. Some devices have a name that the user can configure (e.g. "Loudest speaker") and some devices have a generic name associated with the manufacturer or device model. + */ + name: string; + /** + * Device type, such as "computer", "smartphone" or "speaker". */ + type: string; + /** + * The current volume in percent. + * Range: 0 - 100 + */ + volume_percent: number | null; + /** + * If this device can be used to set the volume. + */ + supports_volume: boolean; +}; + +export type Actions = { + /** Interrupting playback. */ + interrupting_playback?: boolean; + /** Pausing. */ + pausing?: boolean; + /** Resuming. */ + resuming?: boolean; + /** Seeking playback location. */ + seeking?: boolean; + /** Skipping to the next context. */ + skipping_next?: boolean; + /** Skipping to the previous context. */ + skipping_prev?: boolean; + /** Toggling repeat context flag. */ + toggling_repeat_context?: boolean; + /** Toggling shuffle flag. */ + toggling_shuffle?: boolean; + /** Toggling repeat track flag. */ + toggling_repeat_track?: boolean; + /** Transfering playback between devices. */ + transferring_playback?: boolean; +}; + +export type Context = { + /** + * The object type, e.g. "artist", "playlist", "album", "show". + */ + type: string; + /** A link to the Web API endpoint providing full details of the track. */ + href: string; + /** External URLs for this context. */ + external_urls: ExternalUrls; + /** The Spotify URI for the context. */ + uri: string; +}; + +/** + * "track" - repeat the current track. \ + * "context" - repeat the current context. \ + * "off" - turn repeat off. + */ +export type RepeatMode = "off" | "track" | "context"; + +export type PlaybackState = { + /** The device that is currently active. */ + device: Device; + repeat_state: RepeatMode; + /** If shuffle is on or off. */ + shuffle_state: boolean; + context: Context | null; + /** Unix Millisecond Timestamp when data was fetched. */ + timestamp: number; + /** Progress into the currently playing track or episode. */ + progress_ms: number | null; + /** If something is currently playing, return true. */ + is_playing: boolean; + item: TrackItem; + /** The object type of the currently playing item. */ + currently_playing_type: "track" | "episode" | "ad" | "unknown"; + /** + * Allows to update the user interface based on which playback actions are available within the current context. + */ + actions: { + disallows: Actions; + }; +}; + +export type Queue = { + /** The currently playing track or episode. */ + currently_playing: TrackItem | null; + /** The tracks or episodes in the queue. Can be empty. */ + queue: TrackItem[]; +}; + +export type PlayHistoryObject = { + /** The track the user listened to. */ + track: Track; + /** The date and time the track was played. */ + played_at: string; + /** The context the track was played from. */ + context: Context; +}; diff --git a/src/playlist/playlist.endpoints.ts b/src/playlist/playlist.endpoints.ts new file mode 100644 index 0000000..d2ce3d8 --- /dev/null +++ b/src/playlist/playlist.endpoints.ts @@ -0,0 +1,421 @@ +import type { NonNullableObject, Prettify } from "../shared.ts"; +import type { Image, PagingObject, PagingOptions } from "../general.types.ts"; +import type { + FeaturedPlaylists, + Playlist, + PlaylistTrack, + SimplifiedPlaylist, + SnapshotResponse, +} from "./playlist.types.ts"; +import type { HTTPClient } from "../client.ts"; + +export type PlaylistFieldsOpts = { + /** + * List of item types that your client supports besides the default track type. + */ + additional_types?: ("track" | "episode")[]; + /** + * Filters for the query: a comma-separated list of the fields to return. + * If omitted, all fields are returned. + */ + fields?: string; +}; + +export type GetPlaylistOpts = Prettify< + PlaylistFieldsOpts & { + /** + * An ISO 3166-1 alpha-2 country code. + * If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + } +>; + +/** + * Get a playlist owned by a Spotify user + * + * @param client Spotify HTTPClient + * @param playlistId The Spotify ID of the playlist + * @param options Additional options for request + */ +export const getPlaylist = async ( + client: HTTPClient, + playlistId: string, + options?: GetPlaylistOpts, +) => { + const res = await client.fetch("/v1/playlists/" + playlistId, { + query: options, + }); + return res.json() as Promise; +}; + +export type ChangePlaylistDetailsBody = { + /** + * The new name for the playlist, for example "My New Playlist Title" + */ + name?: string; + /** + * If true the playlist will be public, if false it will be private. + */ + public?: boolean; + /** + * If true, the playlist will become collaborative and other users will be able to modify the playlist in their Spotify client. + * + * Note: You can only set collaborative to true on non-public playlists. + */ + collaborative?: boolean; + /** + * Value for playlist description as displayed in Spotify Clients and in the Web API. + */ + description?: string; +}; + +/** + * Change a playlist's name and public/private state. (The user must, of course, own the playlist.) + * + * @param client Spotify HTTPClient + * @param playlistId The Spotify ID of the playlist. + * @param body Changes you want to make to the playlist + */ +export const changePlaylistDetails = ( + client: HTTPClient, + playlistId: string, + body: ChangePlaylistDetailsBody, +) => { + return client.fetch("/v1/playlist/" + playlistId, { method: "PUT", body }); +}; + +export type GetPlaylistTracksOpts = Prettify< + PlaylistFieldsOpts & PagingOptions & { + /** + * An ISO 3166-1 alpha-2 country code. + * If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + } +>; + +/** + * Get full details of the items of a playlist owned by a Spotify user + * + * @param client Spotify HTTPClient + * @param playlistId The Spotify ID of the playlist. + * @param options Additional options for request + */ +export const getPlaylistTracks = async ( + client: HTTPClient, + playlistId: string, + options?: GetPlaylistTracksOpts, +) => { + const res = await client.fetch(`/v1/playlists/${playlistId}/tracks`, { + query: options, + }); + return res.json() as Promise>; +}; + +/** + * Add one or more items to a user's playlist + * + * @param client Spotify HTTPClient + * @param playlistId The Spotify ID of the playlist + * @param uris List of Spotify URIs to add, can be track or episode URIs + * @param position The position to insert the items, a zero-based index + */ +export const addItemsToPlaylist = async ( + client: HTTPClient, + playlistId: string, + uris: string[], + position?: number, +) => { + const res = await client.fetch(`/v1/playlists/${playlistId}/tracks`, { + method: "POST", + query: { uris, position }, + }); + return res.json() as Promise; +}; + +/** + * Add one item to a user's playlist + * + * @param client Spotify HTTPClient + * @param playlistId The Spotify ID of the playlist + * @param uri Spotify URI to add, can be track or episode URI + * @param position The position to insert the item, a zero-based index + */ +export const addItemToPlaylist = ( + client: HTTPClient, + playlistId: string, + uri: string, + position?: number, +) => { + return addItemsToPlaylist(client, playlistId, [uri], position); +}; + +export type ReorderPlaylistItemsOpts = { + /** + * The position of the first item to be reordered. + */ + range_start?: number; + /** + * The position where the items should be inserted. + */ + insert_before?: number; + /** + * The amount of items to be reordered. Defaults to 1 if not set. + * The range of items to be reordered begins from the `range_start` position, and includes the `range_length` subsequent items. + */ + range_length?: number; + /** + * The playlist's snapshot ID against which you want to make the changes. + */ + snapshot_id?: string; +}; + +/** + * Reorder items in a playlist depending on the request's parameters. + * + * @param client Spotify HTTPClient + * @param playlistId The Spotify ID of the playlist. + * @param options Additional options for request + */ +export const reorderPlaylistItems = async ( + client: HTTPClient, + playlistId: string, + options?: ReorderPlaylistItemsOpts, +) => { + const res = await client.fetch(`/v1/playlists/${playlistId}/tracks`, { + method: "PUT", + body: options, + }); + return res.json() as Promise; +}; + +/** + * Replace items in a playlist. Replacing items in a playlist will overwrite its existing items. + * This operation can be used for replacing or clearing items in a playlist. + * + * @param client Spotify HTTPClient + * @param playlistId The Spotify ID of the playlist. + * @param uris List of Spotify URIs to set, can be track or episode URIs. A maximum of 100 items can be set in one request. + */ +export const replacePlaylistItems = async ( + client: HTTPClient, + playlistId: string, + uris: string[], +) => { + const res = await client.fetch(`/v1/playlists/${playlistId}/tracks`, { + method: "PUT", + body: { uris }, + }); + return res.json() as Promise; +}; + +/** + * Remove one or more items from a user's playlist. + * + * @param client Spotify HTTPClient + * @param playlistId The Spotify ID of the playlist. + * @param uris List of Spotify URIs to set, can be track or episode URIs. A maximum of 100 items can be set in one request. + * @param snapshotId The playlist's snapshot ID against which you want to make the changes. + */ +export const removePlaylistItems = async ( + client: HTTPClient, + playlistId: string, + uris: string[], + snapshotId?: string, +) => { + const res = await client.fetch(`/v1/playlists/${playlistId}/tracks`, { + method: "DELETE", + body: { + tracks: uris.map((uri) => ({ uri })), + snapshot_id: snapshotId, + }, + }); + return res.json() as Promise; +}; + +/** + * Remove one item from a user's playlist. + * + * @param client Spotify HTTPClient + * @param playlistId The Spotify ID of the playlist. + * @param uri Spotify URI to set, can be track or episode URIs. + * @param snapshotId The playlist's snapshot ID against which you want to make the changes. + */ +export const removePlaylistItem = ( + client: HTTPClient, + playlistId: string, + uri: string, + snapshotId?: string, +) => { + return removePlaylistItems(client, playlistId, [uri], snapshotId); +}; + +/** + * Get a list of the playlists owned or followed by the current Spotify user. + * + * @param client Spotify HTTPClient + * @param options Additional options for request + */ +export const getCurrentUsersPlaylists = async ( + client: HTTPClient, + options?: PagingOptions, +) => { + const res = await client.fetch("/v1/me/playlists", { query: options }); + return res.json() as Promise>; +}; + +/** + * Get a list of the playlists owned or followed by a Spotify user. + * + * @param client Spotify HTTPClient + * @param userId The user's Spotify user ID. + * @param options Additional options for request + */ +export const getUsersPlaylists = async ( + client: HTTPClient, + userId: string, + options?: PagingOptions, +) => { + const res = await client.fetch(`/v1/users/${userId}/playlists`, { + query: options, + }); + return res.json() as Promise>; +}; + +export type CreatePlaylistBody = { + /** + * The name for the new playlist, for example "Your Coolest Playlist". This name does not need to be unique; a user may have several playlists with the same name. + */ + name: string; + /** + * Defaults to true. If true the playlist will be public, if false it will be private. To be able to create private playlists, the user must have granted the playlist-modify-private scope + */ + public?: boolean; + /** + * Defaults to false. If true the playlist will be collaborative. Note: to create a collaborative playlist you must also set public to false. To create collaborative playlists you must have granted `playlist-modify-private` and `playlist-modify-public` scopes. + */ + collaborative?: boolean; + /** + * Value for playlist description as displayed in Spotify Clients and in the Web API. + */ + description?: string; +}; + +/** + * Create a playlist for a Spotify user. (The playlist will be empty until you add tracks.) + * + * @param client Spotify HTTPClient + * @param userId The user's Spotify user ID. + * @param body Data that will be assinged to new playlist + */ +export const createPlaylist = async ( + client: HTTPClient, + userId: string, + body: CreatePlaylistBody, +) => { + const res = await client.fetch(`/v1/users/${userId}/playlists`, { + method: "POST", + body, + }); + return res.json() as Promise; +}; + +export type GetFeaturedPlaylistsOpts = Prettify< + PagingOptions & { + /** + * A country: an ISO 3166-1 alpha-2 country code. Provide this parameter if you want the list of returned items to be relevant to a particular country. If omitted, the returned items will be relevant to all countries. + */ + country?: string; + /** + * The desired language, consisting of a lowercase ISO 639-1 language code and an uppercase ISO 3166-1 alpha-2 country code, joined by an underscore. For example: es_MX, meaning "Spanish (Mexico)". Provide this parameter if you want the results returned in a particular language (where available). + * + * @example "sv_SE" + */ + locale?: string; + /** + * A timestamp in ISO 8601 format: yyyy-MM-ddTHH:mm:ss. Use this parameter to specify the user's local time to get results tailored for that specific date and time in the day. If not provided, the response defaults to the current UTC time. Example: "2014-10-23T09:00:00" for a user whose local time is 9AM. If there were no featured playlists (or there is no data) at the specified time, the response will revert to the current UTC time. + * + * @example "2014-10-23T09:00:00" + */ + timestamp?: string; + } +>; + +/** + * Get a list of Spotify featured playlists (shown, for example, on a Spotify player's 'Browse' tab). + * + * @param client Spotify HTTPClient + * @param options Additional options for request + */ +export const getFeaturedPlaylists = async ( + client: HTTPClient, + options?: GetFeaturedPlaylistsOpts, +) => { + const res = await client.fetch("/v1/browse/featured-playlists", { + query: options, + }); + return res.json() as Promise; +}; + +export type GetCategorysPlaylistsOpts = Prettify< + PagingOptions & { + /** + * A country: an ISO 3166-1 alpha-2 country code. Provide this parameter to ensure that the category exists for a particular country. + * @example "SE" + */ + country: string; + } +>; + +/** + * Get a list of Spotify playlists tagged with a particular category. + * + * @param client Spotify HTTPClient + * @param categoryId The Spotify category ID for the category. + * @param options Additional options for request + */ +export const getCategoryPlaylists = async ( + client: HTTPClient, + categoryId: string, + options?: GetCategorysPlaylistsOpts, +) => { + const res = await client.fetch( + `/v1/browse/categories/${categoryId}/playlists`, + { query: options }, + ); + return res.json() as Promise; +}; + +/** + * Get the current image associated with a specific playlist. + * + * @param client Spotify HTTPClient + */ +export const getPlaylistCoverImage = async ( + client: HTTPClient, + playlistId: string, +) => { + const res = await client.fetch(`/v1/playlists/${playlistId}/images`); + return res.json() as Promise[]>; +}; + +/** + * Upload custom images to the playlist. + * + * @param playlistId The Spotify ID of the playlist. + * @param image The image should contain a Base64 encoded JPEG image data, maximum payload size is 256 KB. + */ +export const uploadPlaylistCoverImage = ( + client: HTTPClient, + playlistId: string, + image: string, +) => { + return client.fetch(`/v1/playlists/${playlistId}/images`, { + method: "PUT", + headers: { + "Content-Type": "image/jpeg", + }, + body: image, + }); +}; diff --git a/src/playlist/playlist.types.ts b/src/playlist/playlist.types.ts new file mode 100644 index 0000000..583a9f1 --- /dev/null +++ b/src/playlist/playlist.types.ts @@ -0,0 +1,132 @@ +import type { + ExternalUrls, + Followers, + Image, + PagingObject, +} from "../general.types.ts"; +import type { UserPublic } from "../user/user.types.ts"; +import type { Track } from "../track/track.types.ts"; + +export type SnapshotResponse = { snapshot_id: string }; + +export interface SimplifiedPlaylist { + /** + * `true` if the owner allows other users to modify the playlist. + */ + collaborative: boolean; + /** + * The playlist description. Only returned for modified, verified playlists, otherwise `null`. + */ + description: string | null; + /** + * Known external URLs for this playlist. + */ + external_urls: ExternalUrls; + + /** + * A link to the Web API endpoint providing full details of the playlist. + */ + href: string; + /** + * The Spotify ID for the playlist. + */ + id: string; + /** + * Images for the playlist. + * The array may be empty or contain up to three images. + * The images are returned by size in descending order. + * + * Be aware that the links will expire in less than one day. + */ + images: Image[]; + /** + * The name of the playlist. + */ + name: string; + /** + * The user who owns the playlist + */ + owner: UserPublic; + /** + * The version identifier for the current playlist. + * Can be supplied in other requests to target a specific playlist version. + */ + snapshot_id: string; + type: "playlist"; + /** + * The Spotify URI for the playlist. + */ + uri: string; + /** A collection containing a link ( href ) to the Web API endpoint where full details of the playlist’s tracks can be retrieved, along with the total number of tracks in the playlist. */ + tracks: TracksReference; +} + +export type TracksReference = { + /** + * A link to the Web API endpoint where full details of the playlist’s tracks can be retrieved. + */ + href: string; + /** The total number of tracks in playlist. */ + total: number; +}; + +/** + * The structure containing the details of the Spotify Track in the playlist. + */ +export interface PlaylistTrack { + /** + * The date and time the track or episode was added. + * Note: some very old playlists may return null in this field. + */ + added_at: string | null; + /** + * The Spotify user who added the track or episode. \ + * Note: some very old playlists may return null in this field. + */ + added_by: { + type: "user"; + /** The Spotify user ID for this user. */ + id: string; + uri: string; + /** Known public external URLs for this user. */ + external_urls: ExternalUrls; + /** Information about the followers of this user. */ + followers?: Followers; + /** A link to the Web API endpoint for this user. */ + href: string; + } | null; + /** + * Whether this track or episode is a local file or not. + */ + is_local: boolean; + primary_color: string | null; + track: Track; +} + +export interface Playlist extends SimplifiedPlaylist { + /** + * Information about the followers of the playlist. + */ + followers: Followers; + /** + * The playlist's public/private status: + * + * `true` => the playlist is public \ + * `false` => the playlist is private \ + * `null` => the playlist status is not relevant + */ + public: boolean | null; + /** + * The tracks of the playlist. + */ + tracks: PagingObject; +} + +export type FeaturedPlaylists = { + /** The message from the featured playlists. */ + message: string; + /** + * The list of the featured playlists wrapped in Paging object. + */ + playlists: PagingObject; +}; diff --git a/src/search/search.endpoints.ts b/src/search/search.endpoints.ts new file mode 100644 index 0000000..c83161f --- /dev/null +++ b/src/search/search.endpoints.ts @@ -0,0 +1,119 @@ +import type { ItemType, PagingObject } from "../general.types.ts"; +import type { SimplifiedAlbum } from "../album/album.types.ts"; +import type { Artist } from "../artist/artist.types.ts"; +import type { Track } from "../track/track.types.ts"; +import type { SimplifiedPlaylist } from "../playlist/playlist.types.ts"; +import type { HTTPClient } from "../client.ts"; +import type { SimplifiedAudiobook } from "../audiobook/audiobook.types.ts"; +import type { SimplifiedEpisode } from "../episode/episode.types.ts"; +import type { SimplifiedShow } from "../show/show.types.ts"; + +type ItemTypeToResultKey = { + album: "albums"; + artist: "artists"; + playlist: "playlists"; + track: "tracks"; + show: "shows"; + episode: "episodes"; + audiobook: "audiobooks"; +}; + +type ItemTypesToSearchResultKeys = T extends + ItemType[] ? Pick[T[number]] + : T extends ItemType ? ItemTypeToResultKey[T] + : never; + +export type SearchResponse = { + tracks: PagingObject; + artists: PagingObject; + albums: PagingObject; + playlists: PagingObject; + shows: PagingObject; + audiobooks: PagingObject; + episodes: PagingObject; +}; + +export type SearchQueries = { + query?: string; + track?: string; + artist?: string; + album?: string; + /** You can filter on a single year or a range (e.g. 1955-1960). */ + year?: number | string; + genre?: string; + upc?: number | string; + isrc?: number | string; + /** + * The `tag:new` filter will return albums released in the past two weeks and `tag:hipster` can be used to return only albums with the lowest 10% popularity. + */ + tag?: "hipster" | "new"; +}; + +type ItemTypeQueries = { + artists: "query" | "artist" | "genre" | "year"; + tracks: "query" | "artist" | "album" | "genre" | "isrc" | "year"; + albums: "query" | "artist" | "album" | "tag" | "year" | "upc"; + playlists: "query"; + shows: "query"; + audiobooks: "query"; + episodes: "query"; +}; + +type SearchQueriesFromItemTypes = Pick< + SearchQueries, + ItemTypeQueries[ + ItemTypesToSearchResultKeys + ] +>; + +export type SearchOptions = { + /** + * If `include_external=audio` is specified it signals that the client can play externally hosted audio content, and marks the content as playable in the response. + * + * By default externally hosted audio content is marked as unplayable in the response. + */ + include_external?: "audio"; + /** + * The maximum number of results to return in each item type. + * Minimum: 1. Maximum: 50. + * + * @default 20 + */ + limit?: number; + /** + * An ISO 3166-1 alpha-2 country code. + * If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + /** + * The index of the first result to return. Use with limit to get the next page of search results. + * Minimum 0. Maximum 1000. + * + * @default 0 + */ + offset?: number; +}; + +/** + * Get Spotify catalog information about albums, artists, playlists, tracks, shows, episodes or audiobooks that match a keyword string. + * + * @param client Spotify HTTPClient + * @param type One or multiple item types to search across + * @param query Your search query + * @param options Additional options for request + */ +export const search = async ( + client: HTTPClient, + type: T, + query: string | SearchQueriesFromItemTypes, + options?: SearchOptions, +): Promise>> => { + const q = typeof query === "string" ? query : Object.entries(query) + .map(([key, value]) => (key === "query" ? value : `${key}:${value}`)) + .join(" "); + + const res = await client.fetch("/v1/search", { + query: { q, type, ...options }, + }); + return res.json() as Promise; +}; diff --git a/src/shared.ts b/src/shared.ts index 06aa3ff..687ff7a 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -1,65 +1,20 @@ -/** - * The interface used to provide access token with the ability to refresh it - */ -export interface IAuthProvider { - refresh: () => Promise; - token?: string; -} - -export type JSONValue = - | null - | string - | number - | boolean - | JSONArray - | JSONObject; -export type JSONArray = JSONValue[]; -export interface JSONObject { - [x: string]: JSONValue | undefined; -} - -export type NonNullableJSON = { - [K in keyof T]: NonNullable; +export type NonNullableObject = { + [K in keyof T]: NonNullable; }; export type SearchParam = - | string - | number - | boolean - | readonly string[] - | readonly number[] - | readonly boolean[]; -export interface SearchParams { - [k: string]: SearchParam | undefined; -} - -/** - * Creates a query string from the object and skips `undefined` values. - */ -export const toQueryString = (obj: T): string => { - const params = new URLSearchParams(); - - for (const key in obj) { - const value = obj[key] as SearchParam | undefined; - if (typeof value !== "undefined") params.set(key, value.toString()); - } + | string + | number + | boolean + | string[] + | number[] + | boolean[] + | undefined; +export type SearchParams = Record; - return params.toString(); -}; - -/** - * Attempts to parse response body as json and return it, otherwise returns response as string. - * If the response body is null or empty, it returns an empty string. - */ -export const parseResponse = async ( - res: Response -): Promise => { - let text = ""; - try { - text = await res.text(); - if (!text) return text; - return JSON.parse(text) as T; - } catch (_) { - return text; - } -}; +export type Prettify = + & { + [K in keyof T]: T[K]; + } + // deno-lint-ignore ban-types + & {}; diff --git a/src/show/show.endpoints.ts b/src/show/show.endpoints.ts new file mode 100644 index 0000000..837deeb --- /dev/null +++ b/src/show/show.endpoints.ts @@ -0,0 +1,182 @@ +import type { HTTPClient } from "../client.ts"; +import type { Prettify } from "../shared.ts"; +import type { SimplifiedEpisode } from "../episode/episode.types.ts"; +import type { PagingObject, PagingOptions } from "../general.types.ts"; +import type { Show, SimplifiedShow } from "./show.types.ts"; + +/** + * Get spotify catalog information for a single show by its unique Spotify ID. + * + * @param client Spotify HTTPClient + * @param showId The Spotify ID of the show + * @param market An ISO 3166-1 alpha-2 country code + */ +export const getShow = async ( + client: HTTPClient, + showId: string, + market?: string, +) => { + const res = await client.fetch("/v1/shows/" + showId, { query: { market } }); + return res.json() as Promise; +}; + +/** + * Get spotify catalog information for multiple shows by their Spotify IDs. + * + * @param client Spotify HTTPClient + * @param showIds List of the Spotify IDs for the shows. Maximum: 20 IDs + * @param market An ISO 3166-1 alpha-2 country code + */ +export const getShows = async ( + client: HTTPClient, + showIds: string[], + market?: string, +) => { + const res = await client.fetch("/v1/shows", { + query: { market, ids: showIds }, + }); + return (await res.json() as { shows: SimplifiedShow }).shows; +}; + +export type GetShowEpisodesOpts = Prettify< + PagingOptions & { + /** + * An ISO 3166-1 alpha-2 country code. + * If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + } +>; + +/** + * Get Spotify catalog information about an show's episodes. + * Optional parameters can be used to limit the number of episodes returned. + * + * @param client Spotify HTTPClient + * @param showId The Spotify ID of the show + * @param options Additional option for request + */ +export const getShowEpisodes = async ( + client: HTTPClient, + showId: string, + options?: GetShowEpisodesOpts, +) => { + const res = await client.fetch(`/v1/shows/${showId}/episodes`, { + query: options, + }); + return res.json() as Promise>; +}; + +export type GetSavedShowsOpts = Prettify< + PagingOptions & { + /** + * An ISO 3166-1 alpha-2 country code. + * If a country code is specified, only content that is available in that market will be returned. + */ + market?: string; + } +>; + +/** + * Get a list of shows saved in the current Spotify user's library. + * + * @param client Spotify HTTPClient + * @param options Additional option for request + */ +export const getSavedShows = async ( + client: HTTPClient, + options?: GetSavedShowsOpts, +) => { + const res = await client.fetch("/v1/me/shows", { query: options }); + return res.json() as Promise< + PagingObject<{ + /** + * The date and time the album was saved Timestamps are returned in ISO 8601 format as Coordinated Universal Time (UTC) with a zero offset: YYYY-MM-DDTHH:MM:SSZ. + */ + added_at: string; + /** + * Information about the show. + */ + show: Show; + }> + >; +}; + +/** + * Save one or more shows to current Spotify user's library. + * + * @param client Spotify HTTPClient + * @param showIds List of the Spotify IDs for the shows. Maximum: 20 + */ +export const saveShows = (client: HTTPClient, showIds: string[]) => { + return client.fetch("/v1/me/shows", { + method: "PUT", + query: { ids: showIds }, + }); +}; + +/** + * Save show to current Spotify user's library. + * + * @param client Spotify HTTPClient + * @param showId The Spotify ID of the show + */ +export const saveShow = (client: HTTPClient, showId: string) => { + return saveShows(client, [showId]); +}; + +/** + * Delete one or more shows from current Spotify user's library. + * + * @param client Spotify HTTPClient + * @param showIds List of the Spotify IDs for the shows. Maximum: 20 + */ +export const removeSavedShows = ( + client: HTTPClient, + showIds: string[], +) => { + return client.fetch("/v1/me/shows", { + method: "DELETE", + query: { + ids: showIds, + }, + }); +}; + +/** + * Remove show from the current user's 'Your Shows' library. + * + * @param client Spotify HTTPClient + * @param showId The Spotify ID of the show + */ +export const removeSavedShow = (client: HTTPClient, showId: string) => { + return removeSavedShows(client, [showId]); +}; + +/** + * Check if one or more shows is already saved in the current Spotify users' 'Your Shows' library. + * + * @param client Spotify HTTPClient + * @param showIds List of the Spotify IDs for the shows. Maximum: 20 + */ +export const checkIfShowsSaved = async ( + client: HTTPClient, + showIds: string[], +) => { + const res = await client.fetch("/v1/me/shows/contains", { + query: { + ids: showIds, + }, + }); + return res.json() as Promise; +}; + +/** + * Check if show is already saved in the current Spotify user's 'Your Shows' library. + * + * @param client Spotify HTTPClient + * @param showId The Spotify ID of the show + */ +export const checkIfShowSaved = async (client: HTTPClient, showId: string) => { + return (await checkIfShowsSaved(client, [showId]))[0]!; +}; diff --git a/src/show/show.types.ts b/src/show/show.types.ts new file mode 100644 index 0000000..fbe615b --- /dev/null +++ b/src/show/show.types.ts @@ -0,0 +1,82 @@ +import type { SimplifiedEpisode } from "../episode/episode.types.ts"; +import type { + Copyright, + ExternalUrls, + Image, + PagingObject, +} from "../general.types.ts"; + +export interface SimplifiedShow { + /** + * A list of the countries in which the track can be played. + */ + available_markets: string[]; + /** + * The copyright statements of the show. + */ + copyrights: Copyright[]; + /** + * The description of the show without html tags. + */ + description: string; + /** + * The description of the show with html tags. + */ + html_description: string; + /** + * Whether or not the show has explicit lyrics. + */ + explicit: boolean; + /** + * External URLs for this show. + */ + external_urls: ExternalUrls; + /** + * A link to the Web API endpoint providing full details of the show. + */ + href: string; + /** + * The Spotify ID for the show. + */ + id: string; + /** + * Images of the show in various sizes, widest first. + */ + images: Image[]; + /** + * True, if the episode is hosted outside of Spotify's CDN. + */ + is_externally_hosted: boolean; + /** + * A list of the languages used in the episode, identified by their ISO 639-1 code. + */ + languages: string[]; + /** + * The media type of the show. + */ + media_type: string; + /** + * The name of the episode. + */ + name: string; + /** + * The publisher of the show. + */ + publisher: string; + type: "show"; + /** + * The Spotify URI for the show. + */ + uri: string; + /** + * The total number of episodes in the show. + */ + total_episodes: number; +} + +export interface Show extends SimplifiedShow { + /** + * The episodes of the show. + */ + episodes: PagingObject; +} diff --git a/src/test_client.ts b/src/test_client.ts new file mode 100644 index 0000000..dfbc1d2 --- /dev/null +++ b/src/test_client.ts @@ -0,0 +1,64 @@ +import * as oauth from "oauth4webapi"; +import { SPOTIFY_AUTH_URL } from "@soundify/web-api"; +import { z } from "zod"; +import { load } from "std/dotenv/mod.ts"; +import { SpotifyClient } from "./client.ts"; + +await load({ export: true }); + +const env = z + .object({ + SPOTIFY_CLIENT_ID: z.string(), + SPOTIFY_CLIENT_SECRET: z.string(), + SPOTIFY_REFRESH_TOKEN: z.string(), + }) + .parse(Deno.env.toObject()); + +const issuer = new URL(SPOTIFY_AUTH_URL); +const authServer = await oauth.processDiscoveryResponse( + issuer, + await oauth.discoveryRequest(issuer), +); + +const authClient: oauth.Client = { + client_id: env.SPOTIFY_CLIENT_ID, + client_secret: env.SPOTIFY_CLIENT_SECRET, + token_endpoint_auth_method: "client_secret_basic", +}; + +const refresher = async () => { + const res = await oauth.refreshTokenGrantRequest( + authServer, + authClient, + env.SPOTIFY_REFRESH_TOKEN, + ); + const data = await oauth.processRefreshTokenResponse( + authServer, + authClient, + res, + ); + + if (oauth.isOAuth2Error(data)) { + throw new Error(data.error + data.error_description); + } + + await Deno.writeTextFile("/tmp/soundify_test_cache.txt", data.access_token); + + return data.access_token; +}; + +const refreshOrGetCachedToken = async () => { + try { + const token = await Deno.readTextFile("/tmp/soundify_test_cache.txt"); + return token; + } catch (_) { + return await refresher(); + } +}; + +const accessToken = await refreshOrGetCachedToken(); + +export const client = new SpotifyClient(accessToken, { + waitForRateLimit: true, + refresher, +}); diff --git a/src/test_env.ts b/src/test_env.ts deleted file mode 100644 index f932cf4..0000000 --- a/src/test_env.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { cleanEnv, str } from "envalid"; -import { SpotifyClient } from "./api/client"; -import { AuthCodeFlow } from "./auth/auth_code"; - -export const env = cleanEnv(process.env, { - SPOTIFY_CLIENT_ID: str(), - SPOTIFY_CLIENT_SECRET: str(), - // Authorization code refresh token with all scopes - SPOTIFY_REFRESH_TOKEN: str() -}); - -export const client = new SpotifyClient( - new AuthCodeFlow({ - client_id: env.SPOTIFY_CLIENT_ID, - client_secret: env.SPOTIFY_CLIENT_SECRET - }).createAuthProvider(env.SPOTIFY_REFRESH_TOKEN) -); diff --git a/src/track/track.endpoints.ts b/src/track/track.endpoints.ts new file mode 100644 index 0000000..7de88eb --- /dev/null +++ b/src/track/track.endpoints.ts @@ -0,0 +1,229 @@ +import type { HTTPClient } from "../client.ts"; +import type { PagingObject, PagingOptions } from "../general.types.ts"; +import type { + AudioAnalysis, + AudioFeatures, + Recomendations, + RecommendationsOptions, + Track, +} from "./track.types.ts"; +import type { Prettify } from "../shared.ts"; + +/** + * Get Spotify catalog information for a single track identified + * by its unique Spotify ID. + * + * @param client Spotify HTTPClient + * @param trackId The Spotify ID for the track + * @param market An ISO 3166-1 alpha-2 country code + */ +export const getTrack = async ( + client: HTTPClient, + trackId: string, + market?: string, +) => { + const res = await client.fetch("/v1/tracks/" + trackId, { + query: { market }, + }); + return res.json() as Promise; +}; + +/** + * Get Spotify catalog information for multiple tracks based on their Spotify IDs. + * + * @param client Spotify HTTPClient + * @param trackIds List of Spotify track IDs. Maximum 50 IDs + * @param market An ISO 3166-1 alpha-2 country code + */ +export const getTracks = async ( + client: HTTPClient, + trackIds: string[], + market?: string, +) => { + const res = await client.fetch("/v1/tracks", { + query: { + ids: trackIds, + market, + }, + }); + return (await res.json() as { tracks: Track[] }).tracks; +}; + +export type GetSavedTracksOpts = Prettify< + PagingOptions & { + /** + * An ISO 3166-1 alpha-2 country code. + */ + marker?: string; + } +>; + +/** + * Get a list of the songs saved in the current + * Spotify user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param options Additional option for request + */ +export const getSavedTracks = async ( + client: HTTPClient, + options: GetSavedTracksOpts, +) => { + const res = await client.fetch("/v1/me/tracks", { + query: options, + }); + return res.json() as Promise>; +}; + +/** + * Save one or more tracks to the current user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param trackIds List of the Spotify track IDs. Maximum 50 IDs + */ +export const saveTracks = (client: HTTPClient, trackIds: string[]) => { + return client.fetch("/v1/me/tracks", { + method: "PUT", + query: { + ids: trackIds, + }, + }); +}; + +/** + * Save track to the current user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param trackId Spotify track ID + */ +export const saveTrack = (client: HTTPClient, trackId: string) => { + return saveTracks(client, [trackId]); +}; + +/** + * Remove one or more tracks from the current user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param trackIds List of the Spotify track IDs. Maximum 50 IDs + */ +export const removeSavedTracks = ( + client: HTTPClient, + trackIds: string[], +) => { + return client.fetch("/v1/me/tracks", { + method: "DELETE", + query: { + ids: trackIds, + }, + }); +}; + +/** + * Remove track from the current user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param trackId Spotify track ID + */ +export const removeSavedTrack = ( + client: HTTPClient, + trackId: string, +) => { + return removeSavedTracks(client, [trackId]); +}; + +/** + * Check if one or more tracks is already saved in the current Spotify user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param track_ids List of the Spotify track IDs. Maximum 50 IDs + */ +export const checkIfTracksSaved = async ( + client: HTTPClient, + track_ids: string[], +) => { + const res = await client.fetch("/v1/me/tracks/contains", { + query: { + ids: track_ids, + }, + }); + return res.json() as Promise; +}; + +/** + * Check if track is already saved in the current Spotify user's 'Your Music' library. + * + * @param client Spotify HTTPClient + * @param trackId Spotify track ID + */ +export const checkIfTrackSaved = async ( + client: HTTPClient, + trackId: string, +) => { + return (await checkIfTracksSaved(client, [trackId]))[0]; +}; + +/** + * Get audio features for multiple tracks based on their Spotify IDs. + * + * @param client Spotify HTTPClient + * @param track_ids List of the Spotify track IDs. Maximum 100 IDs + */ +export const getTracksAudioFeatures = async ( + client: HTTPClient, + track_ids: string[], +) => { + const res = await client.fetch("/v1/audio-features", { + query: { + ids: track_ids, + }, + }); + return (await res.json() as { audio_features: AudioFeatures[] }) + .audio_features; +}; + +/** + * Get audio features for a track based on its Spotify ID. + * + * @param client Spotify HTTPClient + * @param trackId Spotify track ID + */ +export const getTrackAudioFeatures = async ( + client: HTTPClient, + trackId: string, +) => { + const res = await client.fetch( + "/v1/audio-features/" + trackId, + ); + return res.json() as Promise; +}; + +/** + * Get a low-level audio analysis for a track in the Spotify catalog. + * The audio analysis describes the track’s structure and musical content, including rhythm, pitch, and timbre. + * + * @param client Spotify HTTPClient + * @param trackId Spotify track ID + */ +export const getTracksAudioAnalysis = async ( + client: HTTPClient, + trackId: string, +) => { + const res = await client.fetch("/v1/audio-analysis/" + trackId); + return res.json() as Promise; +}; + +/** + * Recommendations are generated based on the available information for a given seed entity and matched against similar artists and tracks. If there is sufficient information about the provided seeds, a list of tracks will be returned together with pool size details. + + * @param client Spotify HTTPClient + * @param options Options and seeds for recomendations + */ +export const getRecommendations = async ( + client: HTTPClient, + options: RecommendationsOptions, +) => { + const res = await client.fetch("/v1/srecommendations", { + query: options, + }); + return res.json() as Promise; +}; diff --git a/src/track/track.schemas.ts b/src/track/track.schemas.ts new file mode 100644 index 0000000..a819573 --- /dev/null +++ b/src/track/track.schemas.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; +import { simplifiedArtistSchema } from "../artist/artist.schemas.ts"; +import { + externalIdsSchema, + externalUrlsSchema, + restrictionsSchema, +} from "../general.shemas.ts"; +import { simplifiedAlbumSchema } from "../album/album.base.schemas.ts"; + +const linkedTrack = z + .object({ + external_urls: externalUrlsSchema, + href: z.string().url(), + id: z.string(), + type: z.literal("track"), + uri: z.string(), + }) + .strict(); + +export const simplifiedTrackSchema = z + .object({ + artists: z.array(simplifiedArtistSchema), + available_markets: z.array(z.string()).optional(), + disc_number: z.number(), + duration_ms: z.number(), + explicit: z.boolean(), + external_urls: externalUrlsSchema, + href: z.string().url(), + is_local: z.boolean(), + is_playable: z.boolean().optional(), + linked_from: linkedTrack.optional(), + name: z.string(), + preview_url: z.string().url().nullable(), + restrictions: restrictionsSchema.optional(), + track_number: z.number().positive(), + id: z.string(), + type: z.literal("track"), + uri: z.string(), + }) + .strict(); + +export const trackSchema = simplifiedTrackSchema.merge( + z.object({ + album: simplifiedAlbumSchema, + artists: z.array(simplifiedArtistSchema), + external_ids: externalIdsSchema, + popularity: z.number().int().min(0).max(100), + }), +); diff --git a/src/track/track.types.ts b/src/track/track.types.ts new file mode 100644 index 0000000..a369c2a --- /dev/null +++ b/src/track/track.types.ts @@ -0,0 +1,672 @@ +import type { SimplifiedAlbum } from "../album/album.types.ts"; +import type { Artist, SimplifiedArtist } from "../artist/artist.types.ts"; +import type { + ExternalIds, + ExternalUrls, + Restrictions, +} from "../general.types.ts"; + +export type LinkedTrack = { + /** + * A map of url name and the url. + */ + external_urls: ExternalUrls; + /** + * The api url where you can get the full details of the linked track. + */ + href: string; + /** + * The Spotify ID for the track. + */ + id: string; + type: "track"; + /** + * The Spotify URI for the track. + */ + uri: string; +}; + +export interface SimplifiedTrack { + /** + * The artists who performed the track. + */ + artists: SimplifiedArtist[]; + /** + * A list of the countries in which the track can be played. + */ + available_markets?: string[]; + /** + * The disc number (usually 1 unless the album consists of more than one disc). + */ + disc_number: number; + /** + * The track length in milliseconds. + */ + duration_ms: number; + /** + * Whether or not the track has explicit lyrics. + */ + explicit: boolean; + /** External URLs for this track. */ + external_urls: ExternalUrls; + /** + * A link to the Web API endpoint providing full details of the track. + */ + href: string; + /** + * Whether or not the track is from a local file. + */ + is_local: boolean; + /** + * If true, the track is playable in the given market. + * Otherwise false. + */ + is_playable?: boolean; + /** + * Part of the response when Track Relinking is applied and is only part of the response if the track linking, in fact, exists. + */ + linked_from?: LinkedTrack; + /** + * The name of the track. + */ + name: string; + /** + * A link to a 30 second preview (MP3 format) of the track. + */ + preview_url: string | null; + /** + * Included in the response when a content restriction is applied. + */ + restrictions?: Restrictions; + /** + * The number of the track. If an album has several discs, the track number is the number on the specified disc. + */ + track_number: number; + /** + * The Spotify ID for the track. + */ + id: string; + type: "track"; + /** + * The Spotify URI for the track. + */ + uri: string; +} + +export interface Track extends SimplifiedTrack { + /** + * The album on which the track appears. + */ + album: SimplifiedAlbum; + /** + * The artists who performed the track. + */ + artists: Artist[]; + /** + * Known external IDs for the track. + */ + external_ids: ExternalIds; + /** + * The popularity of the track. + * The value will be between 0 and 100, with 100 being the most popular. + */ + popularity: number; +} + +export interface AudioFeatures { + /** + * A confidence measure from 0.0 to 1.0 of whether the track is acoustic. 1.0 represents high confidence the track is acoustic. + */ + acousticness: number; + /** + * A URL to access the full audio analysis of this track. An access token is required to access this data. + */ + analysis_url: string; + /** + * Danceability describes how suitable a track is for dancing based on a combination of musical elements including tempo, rhythm stability, beat strength, and overall regularity. + * A value of 0.0 is least danceable and 1.0 is most danceable. + */ + danceability: number; + /** + * The duration of the track in milliseconds. + */ + duration_ms: number; + /** + * Energy is a measure from 0.0 to 1.0 and represents a perceptual measure of intensity and activity. + * Typically, energetic tracks feel fast, loud, and noisy. For example, death metal has high energy, while a Bach prelude scores low on the scale. + * Perceptual features contributing to this attribute include dynamic range, perceived loudness, timbre, onset rate, and general entropy. + */ + energy: number; + /** + * The Spotify ID for the track. + */ + id: string; + /** + * Predicts whether a track contains no vocals. "Ooh" and "aah" sounds are treated as instrumental in this context. + * Rap or spoken word tracks are clearly "vocal". The closer the instrumentalness value is to 1.0, the greater likelihood the track contains no vocal content. + * Values above 0.5 are intended to represent instrumental tracks, but confidence is higher as the value approaches 1.0. + */ + instrumentalness: number; + /** + * The key the track is in. Integers map to pitches using standard Pitch Class notation. + * E.g. 0 = C, 1 = C♯/D♭, 2 = D, and so on. + */ + key: number; + /** + * Detects the presence of an audience in the recording. Higher liveness values represent an increased probability that the track was performed live. + * A value above 0.8 provides strong likelihood that the track is live. + */ + liveness: number; + /** + * The overall loudness of a track in decibels (dB). Loudness values are averaged across the entire track and are useful for comparing relative loudness of tracks. + * Loudness is the quality of a sound that is the primary psychological correlate of physical strength (amplitude). Values typical range between -60 and 0 db. + */ + loudness: number; + /** + * Mode indicates the modality (major or minor) of a track, the type of scale from which its melodic content is derived. + * Major is represented by 1 and minor is 0. + */ + mode: number; + /** + * Speechiness detects the presence of spoken words in a track. The more exclusively speech-like the recording (e.g. Talk show, audio book, poetry), the closer to 1.0 the attribute value. + * Values above 0.66 describe tracks that are probably made entirely of spoken words. Values between 0.33 and 0.66 describe tracks that may contain both music and speech, either in sections or layered, including such cases as rap music. + * Values below 0.33 most likely represent music and other non-speech-like tracks. + */ + speechiness: number; + /** + * The overall estimated tempo of a track in beats per minute (BPM). In musical terminology, tempo is the speed or pace of a given piece and derives directly from the average beat duration. + */ + tempo: number; + /** + * An estimated overall time signature of a track. The time signature (meter) is a notational convention to specify how many beats are in each bar (or measure). + */ + time_signature: number; + /** + * A link to the Web API endpoint providing full details of the track. + */ + track_href: string; + type: "audio_features"; + /** + * The Spotify URI for the track. + */ + uri: string; + /** + * A measure from 0.0 to 1.0 describing the musical positiveness conveyed by a track. Tracks with high valence sound more positive (e.g. Happy, cheerful, euphoric), while tracks with low valence sound more negative (e.g. Sad, depressed, angry). + */ + valence: number; +} + +interface AudioAnalysisMeta { + /** + * The version of the Analyzer used to analyze this track. + */ + analyzer_version: string; + /** + * The platform used to read the track's audio data. + */ + platform: string; + /** + * A detailed status code for this track. If analysis data is missing, this code may explain why. + */ + detailed_status: string; + /** + * The return code of the analyzer process. + * 0 if successful, 1 if any errors occurred. + */ + status_code: 0 | 1; + /** + * The Unix timestamp (in seconds) at which this track was analyzed. + */ + timestamp: number; + /** + * The amount of time taken to analyze this track. + */ + analysis_time: number; + /** + * The method used to read the track's audio data. + */ + input_process: string; +} + +interface AudioAnalysisTrack { + /** + * The exact number of audio samples analyzed from this track. + */ + num_samples: number; + /** + * Length of the track in seconds. + */ + duration: number; + /** + * This field will always contain the empty string. + */ + sample_md5: ""; + /** + * An offset to the start of the region of the track that was analyzed. + * (As the entire track is analyzed, this should always be 0.) + */ + offset_seconds: number; + /** + * The length of the region of the track was analyzed, if a subset of the track was analyzed. + * (As the entire track is analyzed, this should always be 0.) + */ + window_seconds: number; + /** + * The sample rate used to decode and analyze this track. + * May differ from the actual sample rate of this track available on Spotify. + */ + analysis_sample_rate: number; + /** + * The number of channels used for analysis. + * If 1, all channels are summed together to mono before analysis. + */ + analysis_channels: number; + /** + * The time, in seconds, at which the track's fade-in period ends. + * If the track has no fade-in, this will be 0.0. + */ + end_of_fade_in: number; + /** + * The time, in seconds, at which the track's fade-out period starts. + * If the track has no fade-out, this should match the track's length. + */ + start_of_fade_out: number; + /** + * The overall loudness of a track in decibels (dB). + * Loudness values are averaged across the entire track and are useful for comparing relative loudness of tracks. Loudness is the quality of a sound that is the primary psychological correlate of physical strength (amplitude). + * + * Values typically range between -60 and 0 db. + */ + loudness: number; + /** + * The overall estimated tempo of a track in beats per minute (BPM). + * In musical terminology, tempo is the speed or pace of a given piece and derives directly from the average beat duration. + */ + tempo: number; + /** + * The confidence, from 0.0 to 1.0, of the reliability of the `tempo`. + */ + tempo_confidence: number; + /** + * An estimated time signature. + * The time signature (meter) is a notational convention to specify how many beats are in each bar (or measure). The time signature ranges from 3 to 7 indicating time signatures of "3/4", to "7/4". + */ + time_signature: number; + /** + * The confidence, from 0.0 to 1.0, of the reliability of the `time_signature`. + */ + time_signature_confidence: number; + /** + * The key the track is in. + * Integers map to pitches using standard Pitch Class notation. + * E.g. 0 = C, 1 = C♯/D♭, 2 = D, and so on. + * If no key was detected, the value is -1. + */ + key: number; + /** + * The confidence, from 0.0 to 1.0, of the reliability of the `key`. + */ + key_confidence: number; + /** + * Mode indicates the modality (major or minor) of a track, the type of scale from which its melodic content is derived. + * Major is represented by 1 and minor is 0. + */ + mode: number; + /** + * The confidence, from 0.0 to 1.0, of the reliability of the `mode`. + */ + mode_confidence: number; + /** + * An Echo Nest Musical Fingerprint (ENMFP) codestring for this track. + */ + codestring: string; + /** + * A version number for the Echo Nest Musical Fingerprint format used in the codestring field. + */ + code_version: number; + /** + * An EchoPrint codestring for this track. + */ + echoprintstring: string; + /** + * A version number for the EchoPrint format used in the echoprintstring field. + */ + echoprint_version: number; + /** + * A Synchstring for this track. + */ + synchstring: string; + /** + * A version number for the Synchstring used in the synchstring field. + */ + synch_version: number; + /** + * A Rhythmstring for this track. + * The format of this string is similar to the Synchstring. + */ + rhythmstring: string; + /** + * A version number for the Rhythmstring used in the rhythmstring field. + */ + rhythm_version: number; +} + +interface TimeInterval { + /** + * The starting point (in seconds) of the time interval. + */ + start: number; + /** + * The duration (in seconds) of the time interval. + */ + duration: number; + /** + * The confidence, from 0.0 to 1.0, of the reliability of the interval. + */ + confidence: number; +} + +interface AudioAnalysisSection extends TimeInterval { + /** + * The overall loudness of the section in decibels (dB). + * Loudness values are useful for comparing relative loudness of sections within tracks. + */ + loudness: number; + /** + * The overall estimated tempo of the section in beats per minute (BPM). + * In musical terminology, tempo is the speed or pace of a given piece and derives directly from the average beat duration. + */ + tempo: number; + /** + * The confidence, from 0.0 to 1.0, of the reliability of the `tempo`. + * Some tracks contain tempo changes or sounds which don't contain tempo (like pure speech) which would correspond to a low value in this field. + */ + tempo_confidence: number; + /** + * The estimated overall key of the section. + * The values in this field ranging from 0 to 11 mapping to pitches using standard Pitch Class notation + * (E.g. 0 = C, 1 = C♯/D♭, 2 = D, and so on). + * If no key was detected, the value is -1. + */ + key: number; + /** + * The confidence, from 0.0 to 1.0, of the reliability of the `key`. + * Songs with many key changes may correspond to low values in this field. + */ + key_confidence: number; + /** + * Indicates the modality (major or minor) of a section, the type of scale from which its melodic content is derived. + * This field will contain a 0 for "minor", a 1 for "major", or a -1 for no result. + */ + mode: number; + /** + * The confidence, from 0.0 to 1.0, of the reliability of the `mode`. + */ + mode_confidence: number; + /** + * An estimated time signature. The time signature (meter) is a notational convention to specify how many beats are in each bar (or measure). + * The time signature ranges from 3 to 7 indicating time signatures of "3/4", to "7/4". + */ + time_signature: number; + /** + * The confidence, from 0.0 to 1.0, of the reliability of the time_signature. + * Sections with time signature changes may correspond to low values in this field. + */ + time_signature_confidence: number; +} + +interface AudioAnalysisSegment extends TimeInterval { + /** + * The onset loudness of the segment in decibels (dB). + */ + loudness_start: number; + /** + * The peak loudness of the segment in decibels (dB). + */ + loudness_max_time: number; + /** + * The segment-relative offset of the segment peak loudness in seconds. + */ + loudness_max: number; + /** + * The offset loudness of the segment in decibels (dB). This value should be equivalent to the loudness_start of the following segment. + */ + loudness_end: number; + /** + * Pitch content is given by a “chroma” vector, corresponding to the 12 pitch classes C, C#, D to B, with values ranging from 0 to 1 that describe the relative dominance of every pitch in the chromatic scale. + */ + pitches: number[]; + /** + * Timbre is the quality of a musical note or sound that distinguishes different types of musical instruments, or voices. + * It is a complex notion also referred to as sound color, texture, or tone quality, and is derived from the shape of a segment’s spectro-temporal surface, independently of pitch and loudness. + */ + timbre: number[]; +} + +export interface AudioAnalysis { + meta: AudioAnalysisMeta; + track: AudioAnalysisTrack; + /** + * The time intervals of the bars throughout the track. + * A bar (or measure) is a segment of time defined as a given number of beats. + */ + bars: TimeInterval[]; + /** + * The time intervals of beats throughout the track. + * A beat is the basic time unit of a piece of music; for example, each tick of a metronome. Beats are typically multiples of tatums. + */ + beats: TimeInterval[]; + /** + * Sections are defined by large variations in rhythm or timbre, e.g. chorus, verse, bridge, guitar solo, etc. Each section contains its own descriptions of `tempo`, `key`, `mode`, `time_signature`, and `loudness`. + */ + sections: AudioAnalysisSection[]; + /** + * Each segment contains a roughly conisistent sound throughout its duration. + */ + segments: AudioAnalysisSegment[]; + /** + * A tatum represents the lowest regular pulse train that a listener intuitively infers from the timing of perceived musical events (segments). + */ + tatums: TimeInterval[]; +} + +export type RecommendationsOptions = { + /** + * List of Spotify IDs for seed artists. Maximum 5 IDs + */ + seed_artists: string[]; + /** + * List of any genres in the set of available genre seeds. Maximum 5 genres + */ + seed_genres: string[]; + /** + * List of Spotify IDs for a seed track. Maximum 5 IDs + */ + seed_tracks: string[]; + /** + * The target size of the list of recommended tracks. + * Minimum: 1. Maximum: 100. Default: 20. + */ + limit?: number; + /** + * An ISO 3166-1 alpha-2 country code + */ + market?: string; + /** + * Range: `>= 0 <= 1` + */ + max_acousticness?: number; + /** + * Range: `>= 0 <= 1` + */ + max_danceability?: number; + max_duration_ms?: number; + /** + * Range: `>= 0 <= 1` + */ + max_energy?: number; + /** + * Range: `>= 0 <= 1` + */ + max_instrumentalness?: number; + /** + * Range: `>= 0 <= 11` + */ + max_key?: number; + /** + * Range: `>= 0 <= 1` + */ + max_liveness?: number; + max_loudness?: number; + /** + * Range: `>= 0 <= 1` + */ + max_mode?: number; + /** + * Range `>= 0 <= 100` + */ + max_popularity?: number; + /** + * Range: `>= 0 <= 1` + */ + max_speechiness?: number; + max_tempo?: number; + max_time_signature?: number; + /** + * Range: `>= 0 <= 1` + */ + max_valence?: number; + /** + * Range: `>= 0 <= 1` + */ + min_acousticness?: number; + /** + * Range: `>= 0 <= 1` + */ + min_danceability?: number; + min_duration_ms?: number; + /** + * Range: `>= 0 <= 1` + */ + min_energy?: number; + /** + * Range: `>= 0 <= 1` + */ + min_instrumentalness?: number; + /** + * Range: `>= 0 <= 11` + */ + min_key?: number; + /** + * Range: `>= 0 <= 1` + */ + min_liveness?: number; + min_loudness?: number; + /** + * Range: `>= 0 <= 1` + */ + min_mode?: number; + /** + * Range `>= 0 <= 100` + */ + min_popularity?: number; + /** + * Range: `>= 0 <= 1` + */ + min_speechiness?: number; + min_tempo?: number; + /** + * Range `<= 11` + */ + min_time_signature?: number; + /** + * Range: `>= 0 <= 1` + */ + min_valence?: number; + /** + * Range: `>= 0 <= 1` + */ + target_acousticness?: number; + /** + * Range: `>= 0 <= 1` + */ + target_danceability?: number; + /** + * Target duration of the track (ms) + */ + target_duration_ms?: number; + /** + * Range: `>= 0 <= 1` + */ + target_energy?: number; + /** + * Range: `>= 0 <= 1` + */ + target_instrumentalness?: number; + /** + * Range: `>= 0 <= 11` + */ + target_key?: number; + /** + * Range: `>= 0 <= 1` + */ + target_liveness?: number; + target_loudness?: number; + /** + * Range: `>= 0 <= 1` + */ + target_mode?: number; + /** + * Range `>= 0 <= 100` + */ + target_popularity?: number; + target_speechiness?: number; + /** + * Target tempo (BPM) + */ + target_tempo?: number; + target_time_signature?: number; + /** + * Range: `>= 0 <= 1` + */ + target_valence?: number; +}; + +export interface RecommendationSeed { + /** + * The number of tracks available after min_* and max_* filters have been applied. + */ + afterFilteringSize: number; + /** + * The number of tracks available after relinking for regional availability. + */ + afterRelinkingSize: number; + /** + * A link to the full track or artist data for this seed. + * + * For tracks this will be a link to a Track Object. \ + * For artists a link to an Artist Object. \ + * For genre seeds, this value will be null. + */ + href: string | null; + /** + * The id used to select this seed. This will be the same as the string used in the `seed_artists`, `seed_tracks` or `seed_genres` parameter. + */ + id: string; + /** + * The number of recommended tracks available for this seed. + */ + initialPoolSize: number; + /** + * The entity type of this seed. + */ + type: "artist" | "track" | "genre"; +} + +export type Recomendations = { + seeds: RecommendationSeed[]; + /** + * An array of track object ordered according to the parameters supplied. + */ + tracks: Track[]; +}; diff --git a/src/user/user.endpoints.ts b/src/user/user.endpoints.ts new file mode 100644 index 0000000..a21a6de --- /dev/null +++ b/src/user/user.endpoints.ts @@ -0,0 +1,404 @@ +import type { Artist } from "../artist/artist.types.ts"; +import type { Track } from "../track/track.types.ts"; +import type { UserPrivate, UserPublic } from "./user.types.ts"; +import type { + CursorPagingObject, + PagingObject, + PagingOptions, +} from "../general.types.ts"; +import type { HTTPClient } from "../client.ts"; +import type { Prettify } from "../shared.ts"; + +/** + * Get detailed profile information about the current user. + * + * @param client Spotify HTTPClient + */ +export const getCurrentUser = async (client: HTTPClient) => { + const res = await client.fetch("/v1/me"); + return res.json() as Promise; +}; + +export type GetUserTopItemsOpts = Prettify< + PagingOptions & { + /** + * Over what time frame the affinities are computed. + * + * "long_term" => calculated from several years of data \ + * "medium_term" => approximately last 6 months) \ + * "short_term" => approximately last 4 weeks + * + * @default "medium_term" + */ + time_range?: "long_term" | "medium_term" | "short_term"; + } +>; + +export type UserTopItemType = "artists" | "tracks"; +export type UserTopItem = Artist | Track; +interface UserTopItemMap extends Record { + artists: Artist; + tracks: Track; +} + +/** + * Get the current user's top artists or tracks + * based on calculated affinity. + * + * @requires `user-top-read` + * + * @param client Spotify HTTPClient + * @param type The type of entity to return. ("artists" or "tracks") + * @param opts Additional option for request + */ +export const getUserTopItems = async ( + client: HTTPClient, + type: T, + options?: GetUserTopItemsOpts, +) => { + const res = await client.fetch("/v1/me/top/" + type, { query: options }); + return res.json() as Promise>; +}; + +/** + * Get the current user's top artists based on calculated affinity. + * + * @requires `user-top-read` + * + * @param client Spotify HTTPClient + * @param opts Additional option for request + */ +export const getUserTopArtists = async ( + client: HTTPClient, + opts: GetUserTopItemsOpts, +) => { + return await getUserTopItems(client, "artists", opts); +}; + +/** + * Get the current user's top tracks based on calculated affinity. + * + * @requires `user-top-read` + * + * @param client Spotify HTTPClient + * @param opts Additional option for request + */ +export const getUserTopTracks = async ( + client: HTTPClient, + opts: GetUserTopItemsOpts, +) => { + return await getUserTopItems(client, "tracks", opts); +}; + +/** + * Get public profile information about a Spotify user. + * + * @param client Spotify HTTPClient + * @param userId Spotify user ID + */ +export const getUser = async (client: HTTPClient, userId: string) => { + const res = await client.fetch("/v1/users/" + userId); + return res.json() as Promise; +}; + +/** + * Add the current user as a follower of a playlist. + * + * @requires `playlist-modify-public` or `playlist-modify-private` + * + * @param client Spotify HTTPClient + * @param playlistId Spotify playlist ID + * @param isPublic If true the playlist will be included in user's public + * playlists, if false it will remain private. By default - true + */ +export const followPlaylist = async ( + client: HTTPClient, + playlistId: string, + isPublic?: boolean, +) => { + await client.fetch(`/v1/playlists/${playlistId}/followers`, { + method: "PUT", + body: { public: isPublic }, + }); +}; + +/** + * Remove the current user as a follower of a playlist. + * + * @requires `playlist-modify-public` or `playlist-modify-private` + * + * @param client Spotify HTTPClient + * @param playlistId Spotify playlist ID + */ +export const unfollowPlaylist = async ( + client: HTTPClient, + playlistId: string, +) => { + await client.fetch(`/v1/playlists/${playlistId}/followers`, { + method: "DELETE", + }); +}; + +export type GetFollowedArtistsOpts = { + /** + * The maximum number of items to return. Minimum: 1. Maximum: 50. + * @default 20 + */ + limit?: number; + /** + * The last artist ID retrieved from the previous request. + */ + after?: string; +}; + +/** + * Get the current user's followed artists. + * + * @requires `user-follow-read` + * + * @param client Spotify HTTPClient + * @param options Additional option for request + */ +export const getFollowedArtists = async ( + client: HTTPClient, + options?: GetFollowedArtistsOpts, +) => { + const res = await client.fetch("/v1/me/following", { + query: { + ...options, + type: "artist", + }, + }); + return ((await res.json()) as { artists: CursorPagingObject }) + .artists; +}; + +/** + * Add the current user as a follower of one or more artists. + * + * @requires `user-follow-modify` + * + * @param client Spotify HTTPClient + * @param artistIds List of Spotify artist IDs. Maximum 50 + */ +export const followArtists = (client: HTTPClient, artistIds: string[]) => { + return client.fetch("/v1/me/following", { + method: "PUT", + query: { + type: "artist", + ids: artistIds, + }, + }); +}; + +/** + * Add the current user as a follower of an artist. + * + * @requires `user-follow-modify` + * + * @param client Spotify HTTPClient + * @param artistId Spotify artist ID + */ +export const followArtist = (client: HTTPClient, artistId: string) => { + return followArtists(client, [artistId]); +}; + +/** + * Add the current user as a follower of one or more Spotify users. + * + * @requires `user-follow-modify` + * + * @param client Spotify HTTPClient + * @param userIds List of Spotify user IDs. Maximum 50 + */ +export const followUsers = (client: HTTPClient, userIds: string[]) => { + return client.fetch("/v1/me/following", { + method: "PUT", + query: { + type: "user", + ids: userIds, + }, + }); +}; + +/** + * Add the current user as a follower of an user. + * + * @requires `user-follow-modify` + * + * @param client Spotify HTTPClient + * @param artist_id Spotify user ID + */ +export const followUser = (client: HTTPClient, userId: string) => { + return followUsers(client, [userId]); +}; + +/** + * Remove the current user as a follower of one or more artists. + * + * @requires `user-follow-modify` + * + * @param client Spotify HTTPClient + * @param artistIds List of Spotify artist IDs. Maximum 50 + */ +export const unfollowArtists = (client: HTTPClient, artistIds: string[]) => { + return client.fetch("/v1/me/following", { + method: "DELETE", + query: { + type: "artist", + ids: artistIds, + }, + }); +}; + +/** + * Remove the current user as a follower of specified artist. + * + * @requires `user-follow-modify` + * + * @param client Spotify HTTPClient + * @param artistId Spotify artist ID + */ +export const unfollowArtist = (client: HTTPClient, artistId: string) => { + return unfollowArtists(client, [artistId]); +}; + +/** + * Remove the current user as a follower of one or more Spotify users. + * + * @requires `user-follow-modify` + * + * @param client Spotify HTTPClient + * @param userIds List of Spotify user IDs. Maximum 50 + */ +export const unfollowUsers = (client: HTTPClient, userIds: string[]) => { + return client.fetch("/v1/me/following", { + method: "DELETE", + query: { + type: "user", + ids: userIds, + }, + }); +}; + +/** + * Remove the current user as a follower of specified Spotify user. + * + * @requires `user-follow-modify` + * + * @param client Spotify HTTPClient + * @param artist_id Spotify user ID + */ +export const unfollowUser = (client: HTTPClient, userId: string) => { + return unfollowUsers(client, [userId]); +}; + +/** + * Check to see if the current user is following one or more artists. + * + * @requires `user-follow-read` + * + * @param client Spotify HTTPClient + * @param artistIds List of Spotify artist IDs. Maximum 50 + */ +export const checkIfUserFollowsArtists = async ( + client: HTTPClient, + artistIds: string[], +) => { + const res = await client.fetch("/v1/me/following/contains", { + query: { + type: "artist", + ids: artistIds, + }, + }); + return res.json() as Promise; +}; + +/** + * Check to see if the current user is following artist. + * + * @requires `user-follow-read` + * + * @param client Spotify HTTPClient + * @param artistId Spotify artist ID + */ +export const checkIfUserFollowsArtist = async ( + client: HTTPClient, + artistId: string, +) => { + return (await checkIfUserFollowsArtists(client, [artistId]))[0]!; +}; + +/** + * Check to see if the current user is following one or more Spotify users. + * + * @requires `user-follow-read` + * + * @param client Spotify HTTPClient + * @param userIds List of Spotify user IDs. Maximum 50 + */ +export const checkIfUserFollowsUsers = async ( + client: HTTPClient, + userIds: string[], +) => { + const res = await client.fetch("/v1/me/following/contains", { + query: { + type: "user", + ids: userIds, + }, + }); + return res.json() as Promise; +}; + +/** + * Check to see if the current user is following artist. + * + * @requires `user-follow-read` + * + * @param client Spotify HTTPClient + * @param userId Spotify user ID + */ +export const checkIfUserFollowsUser = async ( + client: HTTPClient, + userId: string, +) => { + return (await checkIfUserFollowsUsers(client, [userId]))[0]!; +}; + +/** + * Check to see if one or more Spotify users are following a specified playlist. + * + * @param client Spotify HTTPClient + * @param userIds List of Spotify user IDs. Maximum: 5 ids. + * @param playlistId Spotify palylist ID + */ +export const checkIfUsersFollowPlaylist = async ( + client: HTTPClient, + userIds: string[], + playlistId: string, +) => { + const res = await client.fetch( + `/v1/playlists/${playlistId}/followers/contains`, + { + query: { + ids: userIds, + }, + }, + ); + return res.json() as Promise; +}; + +/** + * Check to see Spotify user is following a specified playlist. + * + * @param client Spotify HTTPClient + * @param userId Spotify user ID + * @param playlistId Spotify palylist ID + */ +export const checkIfUserFollowsPlaylist = async ( + client: HTTPClient, + userId: string, + playlistId: string, +) => { + return (await checkIfUsersFollowPlaylist(client, [userId], playlistId))[0]!; +}; diff --git a/src/user/user.types.ts b/src/user/user.types.ts new file mode 100644 index 0000000..1cc589f --- /dev/null +++ b/src/user/user.types.ts @@ -0,0 +1,103 @@ +import type { ExternalUrls, Followers, Image } from "../general.types.ts"; + +/** + * The spotify api object containing details of a user's public details. + */ +export interface UserPublic { + /** + * The name displayed on the user's profile. + * `null` if not available + */ + display_name: string | null; + /** + * Known external URLs for this user. + */ + external_urls: ExternalUrls; + /** + * Information about the followers of the user. + */ + followers?: Followers; + /** + * A link to the Web API endpoint for this user. + */ + href: string; + /** + * The Spotify user ID for the user. + */ + id: string; + type: "user"; + /** + * The user's profile image. + */ + images?: Image[]; + /** + * The Spotify URI for the user. + */ + uri: string; +} + +/** + * The product type in the User object. + */ +export type UserProductType = "free" | "open" | "premium"; + +/** + * The spotify api object containing the information of explicit content. + */ +export type ExplicitContentSettings = { + /** + * When true, indicates that explicit content should not be played. + */ + filter_enabled: boolean; + /** + * When true, indicates that the explicit content setting is locked + * and can't be changed by the user. + */ + filter_locked: boolean; +}; + +/** + * The spotify api object containing details of a user's public and private details. + * + * For complete information, you might consider including scopes: `user-read-private`, `user-read-email`. + */ +export interface UserPrivate extends UserPublic { + /** + * The country of the user, as set in the user's account profile. + * An ISO 3166-1 alpha-2 country code. + * + * @requires `user-read-private` + */ + country?: string; + /** + * The user's email address, as entered by the user when creating + * their account. + * + * _Important_! _This email address is unverified_; + * there is no proof that it actually belongs to the user. + * + * @requires `user-read-email` + */ + email?: string; + /** + * The user's explicit content settings. + * + * @requires `user-read-private` + */ + explicit_content?: ExplicitContentSettings; + /** + * The user's Spotify subscription level: "premium", "free", etc. + * (The subscription level "open" can be considered the same as "free".) + * + * @requires `user-read-private` + */ + product?: UserProductType; + /** + * The user's profile image. + */ + images: Image[]; + /** + * Information about the followers of the user. + */ + followers: Followers; +} diff --git a/src/vite.env.d.ts b/src/vite.env.d.ts deleted file mode 100644 index 8c80dee..0000000 --- a/src/vite.env.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -/// - -declare const __IS_NODE__: boolean; diff --git a/tsconfig.json b/tsconfig.json index 59e2a16..221ff09 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,34 +1,28 @@ { - "compilerOptions": { - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "noImplicitAny": true, - "noEmitOnError": true, - "noImplicitReturns": true, - "noUnusedParameters": true, - "strict": true, - "lib": [ - "DOM", - "ESNext", - "DOM.Iterable", - ], - "noUnusedLocals": true, - "moduleResolution": "node", - "module": "ESNext", - "target": "ESNext", - "outDir": "types", - "baseUrl": ".", - "isolatedModules": true, - "skipLibCheck": true, - "ignoreDeprecations": "5.0", - "allowUnreachableCode": false, - "allowUnusedLabels": false - }, - "include": [ - "./src/**/*.ts" - ], - "exclude": [ - "node_modules", - "./src/**/*.test.ts" - ] + "compilerOptions": { + "baseUrl": ".", + "strict": true, + "moduleResolution": "Node", + "isolatedModules": true, + "lib": [ + "DOM", + "ESNext", + "DOM.Iterable" + ], + "module": "ESNext", + "target": "ESNext", + "outDir": "dist", + "allowImportingTsExtensions": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noEmitOnError": true, + "noImplicitReturns": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "noUncheckedIndexedAccess": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "skipLibCheck": true, + } } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index 27608cf..0000000 --- a/vite.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineConfig } from "vite"; - -const __IS_NODE__ = process.env.__IS_NODE__ === "true" ?? true; - -export default defineConfig({ - define: { - __IS_NODE__ - }, - build: { - lib: { - entry: "./src/index.ts", - formats: ["es", "cjs"], - fileName: __IS_NODE__ ? "server" : "browser" - }, - emptyOutDir: false, - rollupOptions: { - external: ["node:crypto"] - } - }, - optimizeDeps: { - exclude: ["node:crypto"] - } -}); diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index c89675d..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - define: { - __IS_NODE__: true - }, - test: { - coverage: { - provider: "c8", - all: true, - src: ["./src"], - reporter: ["html", ["lcov", { file: "coverage.lcov" }]] - } - } -});