diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..f5b56f4 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,32 @@ +# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [12.x, 14.x, 16.x, 17.2] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run prettier + - run: npm test + - run: npm run build diff --git a/.gitignore b/.gitignore index d99c9e7..a9dda7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ node_modules/ -*.log \ No newline at end of file +*.log +types/ +commonjs/index.js +browser/index.js +node-esm/index.js +coverage/ +.DS_Store \ No newline at end of file diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..36af219 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..bf0824e --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +*.log \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..4fd0219 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..007db7b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +commonjs/**/*.js +coverage/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..bcec9b5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid" +} diff --git a/HISTORY.md b/HISTORY.md index a57bf4b..e443eb0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,181 +1,181 @@ -1.2.2 / 2021-02-04 -================== - * Fixed accept headers for non-json data - -1.2.1 / 2017-09-04 -================== - * Fixed bug in image downloads (and any url different from `api.discogs.com`) - -1.2.0 / 2017-06-07 -================== - * Query parameter is now optional for `database.search()` - * Implemented different request limits for authenticated and non-authenticated clients - -1.1.0 / 2017-02-23 -================== - * Implemented new Discogs rate limiting headers. The rate limit param in a callback now looks like: `{ limit: 240, used: 1, remaining: 239 }` - -1.0.2 / 2016-11-10 -================== - * Added `collection().getReleaseInstances()` - -1.0.1 / 2016-11-03 -================== - * Fixed issue with `database().search()` when using a `Promise` - -1.0.0 / 2016-10-27 -================== - * When no callback is provided, all API functions now return a native JS `Promise` - * Removed the non get/set method calls like `database.release(...)` deprecated in release `0.8.0` - -0.9.1 / 2016-10-24 -================== - * Upgraded OAuth library to `oauth-1.0a` v2.0.0 - -0.9.0 / 2016-06-15 -================== - * Added `user().getLists()` - * Added the new `user().list()` namespace for other list functions. Currently only contains `getItems()`. - -0.8.0 / 2016-04-13 -================== - * Added the new release rating endpoints - * Changed a lot of function names to more consistent ones. Old function calls still work, but a deprecation - notice is shown on the console and the old function names will be removed in the next major version. - -0.7.2 / 2016-02-09 -================== - * Fixed default maximum number of requests per minute - -0.7.1 / 2016-01-26 -================== - * Added `database().labelReleases()` function - -0.7.0 / 2016-01-25 -================== - * The `util.queue` object literal has been replaced by a minimal `util.Queue` class - * `DiscogsClient` now has two new config params `requestLimit` and `requestLimitInterval` - -0.6.8 / 2015-11-06 -================== - * Dependency updates - * Fixed a minor bug in `DiscogsClient.authenticated()` - -0.6.7 / 2015-09-15 -================== - * Fixed regression bug from version `0.6.6` involving double URL query string encoding in `database().search()` - -0.6.6 / 2015-08-31 -================== - * An empty search query in `database().search()` will no longer add the `q=` param to the search URL - -0.6.5 / 2015-08-27 -================== - * Fixed a bug in `util.merge()` - -0.6.4 / 2015-07-09 -================== - * Prevent `JSON.parse()` crash when the Discogs API returns HTML instead of json (maintainance mode) - * Added Discogs API version to `DiscogsClient` config (only the default `v2` is supported at the moment) - -0.6.3 / 2015-05-28 -================== - * Updated `oauth-1.0a` dependency - -0.6.2 / 2015-02-25 -================== - * `database().image()` now requires the full image url as the first parameter due to the new Discogs image cluster - * Local request throttling by `disconnect` has been disabled for `database().image()` - -0.6.1 / 2015-02-17 -================== - * Added setting output format for user, artist and label profiles through `DiscogsClient.setConfig({outputFormat: 'html'})` - -0.6.0 / 2015-01-19 -================== - * OAuth authentication is no longer embedded in `DiscogsClient` - * Added OAuth signature method configuration option - * Added support for the new `Discogs Auth` authentication methods - * Changed default OAuth signature method to `PLAINTEXT` due to problems with `HMAC-SHA1` + database search - -0.5.3 / 2014-12-02 -================== - * Fixed incorrect assumption that a Discogs order ID is numeric in `marketplace().orders()` - -0.5.2 / 2014-10-30 -================== - * Fixed incorrect reference to `this` from within a callback function in `DiscogsClient.about()` - * The internal `oauth` object of `DiscogsClient` now only gets 3 status values: `null`, `request` and `access` - -0.5.1 / 2014-10-29 -================== - * Fixed a test which was failing due to changes in `0.5.0` and `npm test` now runs the tests - * Added the possibility to set a custom configuration object with `DiscogsClient.setConfig()` for Browserify + CORS or Proxy use cases - * Updated `README.md` to explain the `app` variable - -0.5.0 / 2014-10-22 -================== - * Replaced some short circuit evaluations and improved the general readability of `client.js` - * Implemented a more elegant way to require OAuth authentication for the `get()`, `post()`, `put()` and `delete()` functions of `DiscogsClient` - * **Breaking change:** `DiscogsClient.getAccessToken()` now only accepts **two** parameters: `verifier` and `callback`. - The former `requestObject` parameter is now taken from the `oauth` property of the `DiscogsClient` instance. - For further info see the updated `README.md` - -0.4.2 / 2014-10-20 -================== - * Fixed `this` scoping in `about()` - * Switched from `http` to the newly implemented `https` Discogs API connection for added security - -0.4.1 / 2014-10-16 -================== - * Fixed "Unexpected token u" error when trying to parse an `undefined` response value to JSON - * `marketplace().fee()` now accepts the price argument as both a number (int/float) and a literal string - -0.4.0 / 2014-10-15 -================== - * Use `strict` - * Added local authentication check for the `database().search()` function - -0.3.4 / 2014-07-30 -================== - * Added `user().contributions()` and `user().submissions()` for the newly implemented endpoints - -0.3.3 / 2014-07-08 -================== - * Discogs has fixed the `/images/` endpoint, so changed `database().image()` accordingly - -0.3.2 / 2014-07-01 -================== - * Added `about()` function to get general info about the Discogs API and the `disconnect` client - -0.3.1 / 2014-06-26 -================== - * Fixed a little bug in the calculation of free positions in the request queue - * Started adding unit tests using `wru` - -0.3.0 / 2014-06-24 -================== - * Added automatic request throttle of 1 request per second queueing up to 10 requests - * Exposed the request queueing functions in `util.queue` - -0.2.1 / 2014-06-20 -================== - * Fixed data encoding bug for gzipped response from `0.2.0` - * First implementation of generic error handling using custom `Error` objects containing the HTTP status code - -0.2.0 / 2014-06-19 -================== - * Implemented/fixed broken `database().image()` function from `0.1.1` - * Added rate limiting header info to the callback params - -0.1.1 / 2014-06-18 -================== - * Added `HISTORY.md` - * Fixed some object reference bugs - * Compacted the `DiscogsClient` constructor - * Added the collection folder functions - * Added the `image` function to the `database` namespace - -0.1.0 / 2014-06-18 -================== - * Initial public release +# 1.2.2 / 2021-02-04 + +- Fixed accept headers for non-json data + +# 1.2.1 / 2017-09-04 + +- Fixed bug in image downloads (and any url different from `api.discogs.com`) + +# 1.2.0 / 2017-06-07 + +- Query parameter is now optional for `database.search()` +- Implemented different request limits for authenticated and non-authenticated clients + +# 1.1.0 / 2017-02-23 + +- Implemented new Discogs rate limiting headers. The rate limit param in a callback now looks like: `{ limit: 240, used: 1, remaining: 239 }` + +# 1.0.2 / 2016-11-10 + +- Added `collection().getReleaseInstances()` + +# 1.0.1 / 2016-11-03 + +- Fixed issue with `database().search()` when using a `Promise` + +# 1.0.0 / 2016-10-27 + +- When no callback is provided, all API functions now return a native JS `Promise` +- Removed the non get/set method calls like `database.release(...)` deprecated in release `0.8.0` + +# 0.9.1 / 2016-10-24 + +- Upgraded OAuth library to `oauth-1.0a` v2.0.0 + +# 0.9.0 / 2016-06-15 + +- Added `user().getLists()` +- Added the new `user().list()` namespace for other list functions. Currently only contains `getItems()`. + +# 0.8.0 / 2016-04-13 + +- Added the new release rating endpoints +- Changed a lot of function names to more consistent ones. Old function calls still work, but a deprecation + notice is shown on the console and the old function names will be removed in the next major version. + +# 0.7.2 / 2016-02-09 + +- Fixed default maximum number of requests per minute + +# 0.7.1 / 2016-01-26 + +- Added `database().labelReleases()` function + +# 0.7.0 / 2016-01-25 + +- The `util.queue` object literal has been replaced by a minimal `util.Queue` class +- `DiscogsClient` now has two new config params `requestLimit` and `requestLimitInterval` + +# 0.6.8 / 2015-11-06 + +- Dependency updates +- Fixed a minor bug in `DiscogsClient.authenticated()` + +# 0.6.7 / 2015-09-15 + +- Fixed regression bug from version `0.6.6` involving double URL query string encoding in `database().search()` + +# 0.6.6 / 2015-08-31 + +- An empty search query in `database().search()` will no longer add the `q=` param to the search URL + +# 0.6.5 / 2015-08-27 + +- Fixed a bug in `util.merge()` + +# 0.6.4 / 2015-07-09 + +- Prevent `JSON.parse()` crash when the Discogs API returns HTML instead of json (maintainance mode) +- Added Discogs API version to `DiscogsClient` config (only the default `v2` is supported at the moment) + +# 0.6.3 / 2015-05-28 + +- Updated `oauth-1.0a` dependency + +# 0.6.2 / 2015-02-25 + +- `database().image()` now requires the full image url as the first parameter due to the new Discogs image cluster +- Local request throttling by `disconnect` has been disabled for `database().image()` + +# 0.6.1 / 2015-02-17 + +- Added setting output format for user, artist and label profiles through `DiscogsClient.setConfig({outputFormat: 'html'})` + +# 0.6.0 / 2015-01-19 + +- OAuth authentication is no longer embedded in `DiscogsClient` +- Added OAuth signature method configuration option +- Added support for the new `Discogs Auth` authentication methods +- Changed default OAuth signature method to `PLAINTEXT` due to problems with `HMAC-SHA1` + database search + +# 0.5.3 / 2014-12-02 + +- Fixed incorrect assumption that a Discogs order ID is numeric in `marketplace().orders()` + +# 0.5.2 / 2014-10-30 + +- Fixed incorrect reference to `this` from within a callback function in `DiscogsClient.about()` +- The internal `oauth` object of `DiscogsClient` now only gets 3 status values: `null`, `request` and `access` + +# 0.5.1 / 2014-10-29 + +- Fixed a test which was failing due to changes in `0.5.0` and `npm test` now runs the tests +- Added the possibility to set a custom configuration object with `DiscogsClient.setConfig()` for Browserify + CORS or Proxy use cases +- Updated `README.md` to explain the `app` variable + +# 0.5.0 / 2014-10-22 + +- Replaced some short circuit evaluations and improved the general readability of `client.js` +- Implemented a more elegant way to require OAuth authentication for the `get()`, `post()`, `put()` and `delete()` functions of `DiscogsClient` +- **Breaking change:** `DiscogsClient.getAccessToken()` now only acceps **two** parameters: `verifier` and `callback`. + The former `requestObject` parameter is now taken from the `oauth` property of the `DiscogsClient` instance. + For further info see the updated `README.md` + +# 0.4.2 / 2014-10-20 + +- Fixed `this` scoping in `about()` +- Switched from `http` to the newly implemented `https` Discogs API connection for added security + +# 0.4.1 / 2014-10-16 + +- Fixed "Unexpected token u" error when trying to parse an `undefined` response value to JSON +- `marketplace().fee()` now accepts the price argument as both a number (int/float) and a literal string + +# 0.4.0 / 2014-10-15 + +- Use `strict` +- Added local authentication check for the `database().search()` function + +# 0.3.4 / 2014-07-30 + +- Added `user().contributions()` and `user().submissions()` for the newly implemented endpoints + +# 0.3.3 / 2014-07-08 + +- Discogs has fixed the `/images/` endpoint, so changed `database().image()` accordingly + +# 0.3.2 / 2014-07-01 + +- Added `about()` function to get general info about the Discogs API and the `disconnect` client + +# 0.3.1 / 2014-06-26 + +- Fixed a little bug in the calculation of free positions in the request queue +- Started adding unit tests using `wru` + +# 0.3.0 / 2014-06-24 + +- Added automatic request throttle of 1 request per second queueing up to 10 requests +- Exposed the request queueing functions in `util.queue` + +# 0.2.1 / 2014-06-20 + +- Fixed data encoding bug for gzipped response from `0.2.0` +- First implementation of generic error handling using custom `Error` objects containing the HTTP status code + +# 0.2.0 / 2014-06-19 + +- Implemented/fixed broken `database().image()` function from `0.1.1` +- Added rate limiting header info to the callback params + +# 0.1.1 / 2014-06-18 + +- Added `HISTORY.md` +- Fixed some object reference bugs +- Compacted the `DiscogsClient` constructor +- Added the collection folder functions +- Added the `image` function to the `database` namespace + +# 0.1.0 / 2014-06-18 + +- Initial public release diff --git a/LICENSE b/LICENSE index b975158..7388aa0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,28 @@ +MIT License + +Copyright (c) 2022 Lion Ralfs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + (The MIT License) -Copyright (c) 2014-2017 B. van Eijck +Copyright (c) 2014-2022 B. van Eijck Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index cafeee1..2f95a72 100644 --- a/README.md +++ b/README.md @@ -1,187 +1,208 @@ -[#2](https://github.com/lionralfs/disconnect/pull/2) is a giant refactor of the [origial library](https://github.com/bartve/disconnect) by doing the following: - -- using ES Modules -- using TypeScript (and generating type declarations) for typed parameters and API results -- removing callbacks in favor of Promises -- using Esbuild to provide a bundle that is consumable by either: - - node via ESM - - node via CommonJS - - browsers (where node-fetch is replaced with native window.fetch) -- adding support for all remaining Discogs endpoints -- adding more tests -- adding docs and type info via JSDoc (for non-TypeScript users) - -## About - -`disconnect` is a [Node.js](http://www.nodejs.org) client library that connects with the [Discogs.com API v2.0](http://www.discogs.com/developers/). - -## Features - - * Covers all API endpoints - * Supports [pagination](http://www.discogs.com/developers/#page:home,header:home-pagination), [rate limiting](http://www.discogs.com/developers/#page:home,header:home-rate-limiting), etc. - * All database, marketplace and user functions implement a standard `function(err, data, rateLimit)` format for the callback or return a - native JS [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) when no callback is provided - * Easy access to protected endpoints with `Discogs Auth` - * Includes OAuth 1.0a tools. Just plug in your consumer key and secret and do the OAuth dance - * API functions grouped in their own namespace for easy access and isolation - -## Installation - -[![NPM](https://nodei.co/npm/disconnect.png?downloads=true)](https://nodei.co/npm/disconnect/) - -## Structure -The global structure of `disconnect` looks as follows: -``` -require('disconnect') -> new Client() -> oauth() - -> database() - -> marketplace() - -> user() -> collection() - -> wantlist() - -> list() - -> util -``` - -## Usage - -### Quick start -Here are some basic usage examples that connect with the public API. Error handling has been left out for demonstrational purposes. - -#### Init - -```javascript -var Discogs = require('disconnect').Client; -``` -#### Go! - -Get the release data for a release with the id 176126. -```javascript -var db = new Discogs().database(); -db.getRelease(176126, function(err, data){ - console.log(data); -}); -``` - -Set your own custom [User-Agent](http://www.discogs.com/developers/#page:home,header:home-general-information). This is optional as when omitted `disconnect` will set a default one with the value `DisConnectClient/x.x.x` where `x.x.x` is the installed version of `disconnect`. -```javascript -var dis = new Discogs('MyUserAgent/1.0'); -``` - -Get page 2 of USER_NAME's public collection showing 75 releases. -The second param is the collection folder ID where 0 is always the "All" folder. -```javascript -var col = new Discogs().user().collection(); -col.getReleases('USER_NAME', 0, {page: 2, per_page: 75}, function(err, data){ - console.log(data); -}); -``` - -### Promises -When no callback is provided, the API functions return a native JS [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) for easy chaining. - -```javascript -var db = new Discogs().database(); -db.getRelease(1) - .then(function(release){ - return db.getArtist(release.artists[0].id); - }) - .then(function(artist){ - console.log(artist.name); - }); -``` - -### Output format -User, artist and label profiles can be formatted in different ways: `plaintext`, `html` and `discogs`. `disconnect` defaults to `discogs`, but the output format can be set for each client instance. -```javascript -// Set the output format to HTML -var dis = new Discogs().setConfig({outputFormat: 'html'}); -``` - -### Discogs Auth -Just provide the client constructor with your preferred way of [authentication](http://www.discogs.com/developers/#page:authentication). -```javascript -// Authenticate by user token -var dis = new Discogs({userToken: 'YOUR_USER_TOKEN'}); - -// Authenticate by consumer key and secret -var dis = new Discogs({ - consumerKey: 'YOUR_CONSUMER_KEY', - consumerSecret: 'YOUR_CONSUMER_SECRET' -}); -``` - -The User-Agent can still be passed for authenticated calls. -```javascript -var dis = new Discogs('MyUserAgent/1.0', {userToken: 'YOUR_USER_TOKEN'}); -``` - -### OAuth -Below are the steps that involve getting a valid OAuth access token from Discogs. Note that in the following examples the `app` variable is an [Express instance](http://expressjs.com/starter/hello-world.html) to handle incoming HTTP requests. - -#### 1. Get a request token -```javascript -app.get('/authorize', function(req, res){ - var oAuth = new Discogs().oauth(); - oAuth.getRequestToken( - 'YOUR_CONSUMER_KEY', - 'YOUR_CONSUMER_SECRET', - 'http://your-script-url/callback', - function(err, requestData){ - // Persist "requestData" here so that the callback handler can - // access it later after returning from the authorize url - res.redirect(requestData.authorizeUrl); - } - ); -}); -``` - -#### 2. Authorize -After redirection to the Discogs authorize URL in step 1, authorize the application. - -#### 3. Get an access token -```javascript -app.get('/callback', function(req, res){ - var oAuth = new Discogs(requestData).oauth(); - oAuth.getAccessToken( - req.query.oauth_verifier, // Verification code sent back by Discogs - function(err, accessData){ - // Persist "accessData" here for following OAuth calls - res.send('Received access token!'); - } - ); -}); -``` - -#### 4. Make OAuth calls -Simply provide the constructor with the `accessData` object persisted in step 3. -```javascript -app.get('/identity', function(req, res){ - var dis = new Discogs(accessData); - dis.getIdentity(function(err, data){ - res.send(data); - }); -}); -``` - -### Images -Image requests themselves don't require authentication, but obtaining the image URLs through, for example, release data does. -```javascript -var db = new Discogs(accessData).database(); -db.getRelease(176126, function(err, data){ - var url = data.images[0].resource_url; - db.getImage(url, function(err, data, rateLimit){ - // Data contains the raw binary image data - require('fs').writeFile('/tmp/image.jpg', data, 'binary', function(err){ - console.log('Image saved!'); - }); - }); -}); -``` - -## Resources - - * [Discogs API documentation](http://www.discogs.com/developers/) - * [The OAuth Bible](http://oauthbible.com/) - -## License - -MIT \ No newline at end of file +[#2](https://github.com/lionralfs/disconnect/pull/2) is a giant refactor of the [origial library](https://github.com/bartve/disconnect) by doing the following: + +- using ES Modules +- using TypeScript (and generating type declarations) for typed parameters and API results +- removing callbacks in favor of Promises +- using Esbuild to provide a bundle that is consumable by either: + - node via ESM + - node via CommonJS + - browsers (where node-fetch is replaced with native window.fetch) +- adding support for all remaining Discogs endpoints +- adding more tests +- adding docs and type info via JSDoc (for non-TypeScript users) + +## About + +`disconnect` is a [Node.js](http://www.nodejs.org) client library that connects with the [Discogs.com API v2.0](http://www.discogs.com/developers/). + +## Features + +- Covers all API endpoints +- Supports [pagination](http://www.discogs.com/developers/#page:home,header:home-pagination), [rate limiting](http://www.discogs.com/developers/#page:home,header:home-rate-limiting), etc. +- All database, marketplace and user functions implement a standard `function(err, data, rateLimit)` format for the callback or return a + native JS [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) when no callback is provided +- Easy access to protected endpoints with `Discogs Auth` +- Includes OAuth 1.0a tools. Just plug in your consumer key and secret and do the OAuth dance +- API functions grouped in their own namespace for easy access and isolation + +## Installation + +[![NPM](https://nodei.co/npm/disconnect.png?downloads=true)](https://nodei.co/npm/disconnect/) + +## Structure + +The global structure of `disconnect` looks as follows: + +``` +require('disconnect') -> new Client() -> oauth() + -> database() + -> marketplace() + -> user() -> collection() + -> wantlist() + -> list() + -> util +``` + +## Usage + +### Quick start + +Here are some basic usage examples that connect with the public API. Error handling has been left out for demonstrational purposes. + +#### Init + +```javascript +var Discogs = require('disconnect').Client; +``` + +#### Go! + +Get the release data for a release with the id 176126. + +```javascript +var db = new Discogs().database(); +db.getRelease(176126, function (err, data) { + console.log(data); +}); +``` + +Set your own custom [User-Agent](http://www.discogs.com/developers/#page:home,header:home-general-information). This is optional as when omitted `disconnect` will set a default one with the value `DisConnectClient/x.x.x` where `x.x.x` is the installed version of `disconnect`. + +```javascript +var dis = new Discogs('MyUserAgent/1.0'); +``` + +Get page 2 of USER_NAME's public collection showing 75 releases. +The second param is the collection folder ID where 0 is always the "All" folder. + +```javascript +var col = new Discogs().user().collection(); +col.getReleases('USER_NAME', 0, { page: 2, per_page: 75 }, function (err, data) { + console.log(data); +}); +``` + +### Promises + +When no callback is provided, the API functions return a native JS [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) for easy chaining. + +```javascript +var db = new Discogs().database(); +db.getRelease(1) + .then(function (release) { + return db.getArtist(release.artists[0].id); + }) + .then(function (artist) { + console.log(artist.name); + }); +``` + +### Output format + +User, artist and label profiles can be formatted in different ways: `plaintext`, `html` and `discogs`. `disconnect` defaults to `discogs`, but the output format can be set for each client instance. + +```javascript +// Set the output format to HTML +var dis = new Discogs().setConfig({ outputFormat: 'html' }); +``` + +### Discogs Auth + +Just provide the client constructor with your preferred way of [authentication](http://www.discogs.com/developers/#page:authentication). + +```javascript +// Authenticate by user token +var dis = new Discogs({ userToken: 'YOUR_USER_TOKEN' }); + +// Authenticate by consumer key and secret +var dis = new Discogs({ + consumerKey: 'YOUR_CONSUMER_KEY', + consumerSecret: 'YOUR_CONSUMER_SECRET', +}); +``` + +The User-Agent can still be passed for authenticated calls. + +```javascript +var dis = new Discogs('MyUserAgent/1.0', { userToken: 'YOUR_USER_TOKEN' }); +``` + +### OAuth + +Below are the steps that involve getting a valid OAuth access token from Discogs. Note that in the following examples the `app` variable is an [Express instance](http://expressjs.com/starter/hello-world.html) to handle incoming HTTP requests. + +#### 1. Get a request token + +```javascript +app.get('/authorize', function (req, res) { + var oAuth = new Discogs().oauth(); + oAuth.getRequestToken( + 'YOUR_CONSUMER_KEY', + 'YOUR_CONSUMER_SECRET', + 'http://your-script-url/callback', + function (err, requestData) { + // Persist "requestData" here so that the callback handler can + // access it later after returning from the authorize url + res.redirect(requestData.authorizeUrl); + } + ); +}); +``` + +#### 2. Authorize + +After redirection to the Discogs authorize URL in step 1, authorize the application. + +#### 3. Get an access token + +```javascript +app.get('/callback', function (req, res) { + var oAuth = new Discogs(requestData).oauth(); + oAuth.getAccessToken( + req.query.oauth_verifier, // Verification code sent back by Discogs + function (err, accessData) { + // Persist "accessData" here for following OAuth calls + res.send('Received access token!'); + } + ); +}); +``` + +#### 4. Make OAuth calls + +Simply provide the constructor with the `accessData` object persisted in step 3. + +```javascript +app.get('/identity', function (req, res) { + var dis = new Discogs(accessData); + dis.getIdentity(function (err, data) { + res.send(data); + }); +}); +``` + +### Images + +Image requests themselves don't require authentication, but obtaining the image URLs through, for example, release data does. + +```javascript +var db = new Discogs(accessData).database(); +db.getRelease(176126, function (err, data) { + var url = data.images[0].resource_url; + db.getImage(url, function (err, data, rateLimit) { + // Data contains the raw binary image data + require('fs').writeFile('/tmp/image.jpg', data, 'binary', function (err) { + console.log('Image saved!'); + }); + }); +}); +``` + +## Resources + +- [Discogs API documentation](http://www.discogs.com/developers/) +- [The OAuth Bible](http://oauthbible.com/) + +## License + +MIT diff --git a/browser/fetch.js b/browser/fetch.js new file mode 100644 index 0000000..7e75782 --- /dev/null +++ b/browser/fetch.js @@ -0,0 +1,2 @@ +export default fetch; +export const Headers = globalThis.Headers; diff --git a/browser/package.json b/browser/package.json new file mode 100644 index 0000000..4eb1270 --- /dev/null +++ b/browser/package.json @@ -0,0 +1,4 @@ +{ + "type": "module", + "types": "../types" +} diff --git a/build.js b/build.js new file mode 100644 index 0000000..a8eaa35 --- /dev/null +++ b/build.js @@ -0,0 +1,49 @@ +import fs from 'fs'; +import { resolve } from 'path'; +import { build } from 'esbuild'; +import alias from 'esbuild-plugin-alias'; + +const PACKAGE = JSON.parse(fs.readFileSync('./package.json', 'utf-8')); + +// build for browser, esm +build({ + entryPoints: ['./lib/index.ts'], + bundle: true, + outfile: 'browser/index.js', + format: 'esm', + platform: 'browser', + target: ['es2020'], + minify: true, + plugins: [ + alias({ + 'node-fetch': resolve('./browser/fetch.js'), + }), + ], + define: { 'process.env.VERSION_NUMBER': `'${PACKAGE.version}'` }, +}).catch(err => console.error(err) || process.exit(1)); + +// build for node, esm +build({ + entryPoints: ['./lib/index.ts'], + bundle: true, + outfile: 'node-esm/index.js', + format: 'esm', + platform: 'node', + target: ['node12'], + minify: false, + define: { 'process.env.VERSION_NUMBER': `'${PACKAGE.version}'` }, + external: ['node-fetch'], +}).catch(err => console.error(err) || process.exit(1)); + +// build for node, commonjs +build({ + entryPoints: ['./lib/index.ts'], + bundle: true, + outfile: 'commonjs/index.js', + format: 'cjs', + target: ['node12'], + platform: 'node', + minify: false, + define: { 'process.env.VERSION_NUMBER': `'${PACKAGE.version}'` }, + external: ['node-fetch'], +}).catch(err => console.error(err) || process.exit(1)); diff --git a/commonjs/package.json b/commonjs/package.json new file mode 100644 index 0000000..09af14e --- /dev/null +++ b/commonjs/package.json @@ -0,0 +1,4 @@ +{ + "type": "commonjs", + "types": "../types" +} diff --git a/index.js b/index.js deleted file mode 100644 index 496cdce..0000000 --- a/index.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -var discogs = module.exports = {}; - -/** - * Expose Discogs utility function library - */ - -discogs.util = require('./lib/util.js'); - -/** - * Expose Discogs Client class - */ - -discogs.Client = require('./lib/client.js'); \ No newline at end of file diff --git a/lib/client.js b/lib/client.js deleted file mode 100644 index 0e50b15..0000000 --- a/lib/client.js +++ /dev/null @@ -1,410 +0,0 @@ -'use strict'; - -var https = require('https'), - zlib = require('zlib'), - url = require('url'), - pkg = require('../package.json'), - error = require('./error.js'), - util = require('./util.js'); - -module.exports = DiscogsClient; - -/** - * Default configuration - */ - -var defaultConfig = { - host: 'api.discogs.com', - port: 443, - userAgent: 'DisConnectClient/' + pkg.version + ' +' + pkg.homepage, - apiVersion: 'v2', - outputFormat: 'discogs', // Possible values: 'discogs' / 'plaintext' / 'html' - requestLimit: 25, // Maximum number of requests to the Discogs API per interval - requestLimitAuth: 60, // Maximum number of requests to the Discogs API per interval when authenticated - requestLimitInterval: 60000 // Request interval in milliseconds -}; - -/** - * The request queue, shared by all DiscogsClient instances - * @type {Queue} - */ - -var queue = require('./queue.js')({ - maxCalls: defaultConfig.requestLimit, - interval: defaultConfig.requestLimitInterval -}); - -/** - * Object constructor - * @param {string} [userAgent] - The name of the user agent to use to make API calls - * @param {object} [auth] - Optional authorization data object - * @return {DiscogsClient} - */ - -function DiscogsClient(userAgent, auth) { - // Allow the class to be called as a function, returning an instance - if (!(this instanceof DiscogsClient)) { - return new DiscogsClient(userAgent, auth); - } - // Set the default configuration - this.config = util.merge({}, defaultConfig); - // Set the custom User Agent when provided - if (typeof userAgent === 'string') { - this.config.userAgent = userAgent; - } - // No userAgent provided, but instead we have an accessObject - if ((arguments.length === 1) && (typeof userAgent === 'object')) { - auth = userAgent; - } - // Set auth data when provided - if (auth && (typeof auth === 'object')) { - queue.setConfig({maxCalls: this.config.requestLimitAuth}); - if (!auth.hasOwnProperty('method')) { - auth.method = 'discogs'; - } - if (!auth.hasOwnProperty('level')) { - if (auth.userToken) { - auth.level = 2; - } else if (auth.consumerKey && auth.consumerSecret) { - auth.level = 1; - } - } - this.auth = util.merge({}, auth); - // Unauthenticated new client instances will decrease the shared request limit - } else { - queue.setConfig({maxCalls: this.config.requestLimit}); - } -} - -/** - * Override the default configuration - * @param {object} customConfig - Custom configuration object for Browserify/CORS/Proxy use cases - * @return {DiscogsClient} - */ -DiscogsClient.prototype.setConfig = function(customConfig) { - util.merge(this.config, customConfig); - queue.setConfig({ - maxCalls: (this.authenticated() ? this.config.requestLimitAuth : this.config.requestLimit), - interval: this.config.requestLimitInterval - }); - return this; -}; - -/** - * Return whether the client is authenticated for the optionally given access level - * @param {number} [level] - Optional authentication level - * @return {boolean} - */ - -DiscogsClient.prototype.authenticated = function(level) { - level = level || 0; - return (!(typeof this.auth === 'undefined') && (this.auth.level > 0) && (this.auth.level >= level)); -}; - -/** - * Test authentication by getting the identity resource for the authenticated user - * @param {function} callback - Callback function receiving the data - * @return {DiscogsClient|Promise} - */ - -DiscogsClient.prototype.getIdentity = function(callback) { - return this.get({url: '/oauth/identity', authLevel: 2}, callback); -}; - -/** - * Get info about the Discogs API and this client - * @param {function} callback - Callback function receiving the data - */ - -DiscogsClient.prototype.about = function(callback) { - var clientInfo = { - version: pkg.version, - userAgent: this.config.userAgent, - authMethod: (this.auth ? this.auth.method : 'none'), - authLevel: (this.auth ? this.auth.level : 0) - }; - if (typeof callback === 'function') { - return this.get('', function(err, data) { - data && (data.disconnect = clientInfo); - callback(err, data); - }); - } - return this.get('').then(function(data) { - data && (data.disconnect = clientInfo); - return data; - }); -}; - -/** - * Send a raw request - * @param {object} options - Request options - * { - * url: '', // May be a relative path when accessing the discogs API - * method: '', // Defaults to GET - * data: {} // POST/PUT data as an object - * } - * @param {function} callback - Callback function receiving the data - * @return {DiscogsClient} - */ - -DiscogsClient.prototype._rawRequest = function(options, callback) { - var data = options.data || null, - method = options.method || 'GET', - urlParts = url.parse(options.url), - encoding = options.encoding || 'utf8'; - - // Build request headers - var headers = { - 'User-Agent': this.config.userAgent, - 'Accept': 'application/vnd.discogs.' + this.config.apiVersion + '.' + this.config.outputFormat + '+json,application/octet-stream', - 'Accept-Encoding': 'gzip,deflate', - 'Host': urlParts.host || this.config.host, - 'Connection': 'close', - 'Content-Length': 0 - }; - - // Add content headers for POST/PUT requests that contain data - if (data) { - if (typeof data === 'object') { - data = JSON.stringify(data); - } // Convert data to a JSON string when data is an object/array - headers['Content-Type'] = 'application/json'; // Discogs accepts data in JSON format - headers['Content-Length'] = Buffer.byteLength(data, 'utf8'); - } - - // Add Authorization header when authenticated (or in the process of authenticating) - if (this.auth && (this.auth.consumerKey || this.auth.userToken)) { - var authHeader = ''; - if (this.auth.method === 'oauth') { - var fullUrl = (urlParts.protocol && urlParts.host) ? urlParts.href : 'https://' + this.config.host + urlParts.path; - authHeader = this.oauth().toHeader(method, fullUrl); - } else if (this.auth.method === 'discogs') { - authHeader = 'Discogs'; - if (this.auth.userToken) { - authHeader += ' token=' + this.auth.userToken; - } else if (this.auth.consumerKey) { - authHeader += ' key=' + this.auth.consumerKey + ', secret=' + this.auth.consumerSecret; - } - } - headers['Authorization'] = authHeader; - } - - // Set the HTTPS request options - var requestOptions = { - host: urlParts.host || this.config.host, - port: urlParts.port || this.config.port, - path: urlParts.path, - method: method, - headers: headers - }; - - // Build the HTTPS request - var req = https.request(requestOptions, function(res) { - var data = '', rateLimit = null, add = function(chunk) { - data += chunk.toString(); - }; - - // Pass the data to the callback and pass an error on unsuccessful HTTP status - var passData = function() { - var err = null, status = parseInt(res.statusCode, 10); - if (status > 399) { // Unsuccessful HTTP status? Then pass an error to the callback - var match = data.match(/^\{"message": "(.+)"\}/i); - err = new error.DiscogsError(status, ((match && match[1]) ? match[1] : null)); - } - callback(err, data, rateLimit); - }; - - // Find and add rate limiting when present - if (res.headers['x-discogs-ratelimit']) { - rateLimit = { - limit: parseInt(res.headers['x-discogs-ratelimit'], 10), - used: parseInt(res.headers['x-discogs-ratelimit-used'], 10), - remaining: parseInt(res.headers['x-discogs-ratelimit-remaining'], 10) - }; - } - - // Get the response content and pass it to the callback - switch (res.headers['content-encoding']) { - case 'gzip': - var gunzip = zlib.createGunzip().on('data', add).on('end', passData); - res.pipe(gunzip); - break; - case 'deflate': - var inflate = zlib.createInflate().on('data', add).on('end', passData); - res.pipe(inflate); - break; - default: - // Set encoding when provided - res.setEncoding(encoding); - res.on('data', add).on('end', passData); - } - }).on('error', function(err) { - callback(err); - }); - - // When present, write the data to the request - if (data) { - req.write(data); - } - - req.end(); - return this; -}; - -/** - * Send a request and parse text response to JSON - * @param {object} options - Request options - * { - * url: '', // May be a relative path when accessing the Discogs API - * method: '', // Defaults to GET - * data: {} // POST/PUT data as an object - * } - * @param {function} [callback] - Callback function receiving the data - * @return {DiscogsClient|Promise} - */ - -DiscogsClient.prototype._request = function(options, callback) { - var client = this, - doRequest = function() { - client._rawRequest(options, function(err, data, rateLimit) { - if (data && options.json && (data.indexOf(' | undefined; + + /** + * @param {Partial<{userAgent: string; auth: Partial}>} [options] + */ + constructor(options: Partial<{ userAgent: string; auth: Partial }> = {}) { + // Set the default configuration + this.config = Object.assign({}, defaultConfig); + + // Set the custom User Agent when provided + if (typeof options.userAgent === 'string') { + this.config.userAgent = options.userAgent; + } + + this.auth = undefined; + + // Set auth data when provided + if (typeof options.auth === 'object') { + this.auth = {}; + let auth: Partial = Object.assign({}, options.auth); + + queue.setConfig({ maxCalls: this.config.requestLimitAuth }); + // use 'discogs' as default method + if (!auth.hasOwnProperty('method')) { + this.auth.method = 'discogs'; + } + + if (!auth.hasOwnProperty('level')) { + if (auth.userToken) { + this.auth.userToken = auth.userToken; + this.auth.level = 2; + } else if (auth.consumerKey && auth.consumerSecret) { + this.auth.consumerKey = this.auth.consumerKey; + this.auth.consumerSecret = this.auth.consumerSecret; + this.auth.level = 1; + } + } + } else { + // Unauthenticated new client instances will decrease the shared request limit + queue.setConfig({ maxCalls: this.config.requestLimit }); + } + } + + /** + * Override the default configuration + * @param {Partial} customConfig - Custom configuration object for Browserify/CORS/Proxy use cases + * @returns {DiscogsClient} + */ + setConfig(customConfig: Partial): DiscogsClient { + merge(this.config, customConfig); + queue.setConfig({ + maxCalls: this.authenticated() ? this.config.requestLimitAuth : this.config.requestLimit, + interval: this.config.requestLimitInterval, + }); + return this; + } + + /** + * Return whether the client is authenticated for the optionally given access level + * @param {number} [level] - Optional authentication level + * @returns {boolean} + */ + authenticated(level: number = 0): boolean { + return typeof this.auth === 'object' && this.auth.level !== undefined && this.auth.level >= level; + } + + /** + * Test authentication by getting the identity resource for the authenticated user + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:user-identity,header:user-identity-identity-get + * + * @example + * await client.user().getIdentity(); + */ + getIdentity = (): Promise> => { + // @ts-ignore + return this.get({ url: '/oauth/identity', authLevel: 2 }); + }; + + /** + * Get info about the Discogs API and this client + */ + async about() { + let clientInfo = { + version: version, + userAgent: this.config.userAgent, + authMethod: this.auth ? this.auth.method : 'none', + authLevel: this.auth ? this.auth.level : 0, + }; + let { data, rateLimit } = await this.get(''); + if (data) { + // @ts-ignore + return { ...data, rateLimit, disconnect: clientInfo }; + } + return data; + } + + /** + * Send a raw request + * @param {RequestOptions} options - Request options + * { + * url: '', // May be a relative path when accessing the discogs API + * method: '', // Defaults to GET + * data: {} // POST/PUT data as an object + * } + * @param {RequestCallback} callback - Callback function receiving the data + * @private + */ + _rawRequest(options: RequestOptions, callback: RequestCallback) { + let data = options.data || null; + let method = options.method || 'GET'; + let requestURL = new URL(options.url, `https://${this.config.host}`); + requestURL.port = this.config.port.toString(); + + // Build request headers + let headers = new Headers({ + 'User-Agent': this.config.userAgent, + Accept: `application/vnd.discogs.${this.config.apiVersion}.${this.config.outputFormat}+json`, + }); + + /** @type {import('node-fetch').RequestInit} */ + let requestOptions: import('node-fetch').RequestInit = { + method: method, + headers: headers, + }; + + // Add content headers for POST/PUT requests that contain data + if (data) { + if (typeof data === 'object') { + // Convert data to a JSON string when data is an object/array + requestOptions.body = JSON.stringify(data); + } + + // Discogs accepts data in JSON format + headers.set('Content-Type', 'application/json'); + } + + // Add Authorization header when authenticated (or in the process of authenticating) + if (this.auth && (this.auth.consumerKey || this.auth.userToken)) { + let authHeader = ''; + if (this.auth.method === 'oauth') { + throw new Error('Not implemented!'); + // let fullUrl = requestURL.toString(); + // authHeader = this.oauth().toHeader(method, fullUrl); + } else if (this.auth.method === 'discogs') { + authHeader = 'Discogs'; + if (this.auth.userToken) { + authHeader += ' token=' + this.auth.userToken; + } else if (this.auth.consumerKey) { + authHeader += ' key=' + this.auth.consumerKey + ', secret=' + this.auth.consumerSecret; + } + } + headers.set('Authorization', authHeader); + } + + fetch(requestURL.toString(), requestOptions) + .then(async res => { + let statusCode = res.status; + + /** @type {RateLimit | undefined} */ + let rateLimit: RateLimit | undefined = undefined; + /** @type {Error | undefined} */ + let err: Error | undefined = undefined; + + // Find and add rate limiting when present + if (res.headers.get('x-discogs-ratelimit')) { + rateLimit = { + limit: Number(res.headers.get('x-discogs-ratelimit')), + used: Number(res.headers.get('x-discogs-ratelimit-used')), + remaining: Number(res.headers.get('x-discogs-ratelimit-remaining')), + }; + } + + // try parsing JSON response + /** @type {any} */ // @ts-ignore + let data: any = await res.json().catch(() => {}); + + if (statusCode > 399) { + // Unsuccessful HTTP status? Then pass an error to the callback + let errorMessage = (data && data.message) || ''; + err = new DiscogsError(statusCode, errorMessage); + } + callback(err, data, rateLimit); + }) + .catch(err => { + callback(err); + }); + } + + /** + * Send a request and parse text response to JSON + * @param {RequestOptions} options - Request options + * @returns {Promise<{data: unknown; rateLimit?: RateLimit}>} + */ + async _request(options: RequestOptions): Promise<{ data: unknown; rateLimit?: RateLimit }> { + // By default, queue requests + if (!options.hasOwnProperty('queue')) { + options.queue = true; + } + // By default, expect responses to be JSON + if (!options.hasOwnProperty('json')) { + options.json = true; + } + + return new Promise((resolve, reject) => { + let doRequest = () => { + this._rawRequest(options, function (err, data, rateLimit) { + (err && reject(err)) || resolve({ data, rateLimit }); + }); + }; + + // Check whether authentication is required + if (options.authLevel && !this.authenticated(options.authLevel)) { + throw new AuthError(); + } + + if (options.queue) { + // Add API request to the execution queue + queue.add((err: any) => { + if (!err) { + doRequest(); + } else { + // Can't add to the queue because it's full + throw err; + } + }); + } else { + // Don't queue, just do the request + doRequest(); + } + }); + } + + /** + * Perform a GET request against the Discogs API + * + * @param {string | Pick} options - Request options object or an url + */ + get(options: string | Pick) { + if (typeof options === 'string') { + options = { url: options }; + } + return this._request(options); + } + + /** + * Perform a POST request against the Discogs API + * @param {string | RequestOptions} options - Request options object or an url + * @param {object} data - POST data + * @returns {Promise} + */ + post(options: string | RequestOptions, data: RequestOptions['data']): Promise { + if (typeof options === 'string') { + options = { url: options }; + } + options.method = 'POST'; + options.data = data; + return this._request(options); + } + + /** + * Perform a PUT request against the Discogs API + * @param {string | RequestOptions} options - Request options object or an url + * @param {object} data - PUT data + * @returns {Promise} + */ + put(options: string | RequestOptions, data: object): Promise { + if (typeof options === 'string') { + options = { url: options }; + } + options.method = 'PUT'; + options.data = data; + return this._request(options); + } + + /** + * Perform a DELETE request against the Discogs API + * @param {string | RequestOptions} options - Request options object or an url + * @returns {Promise} + */ + delete(options: string | RequestOptions): Promise { + if (typeof options === 'string') { + options = { url: options }; + } + options.method = 'DELETE'; + return this._request(options); + } + + /** + * Get an instance of the Discogs OAuth class + * @returns {DiscogsOAuth} + */ + // oauth() { + // return new OAuth(this.auth); + // } + + /** + * Expose the database functions and pass the current instance + * @returns {ReturnType} + */ + database(): ReturnType { + return database(this); + } + + /** + * Expose the marketplace functions and pass the current instance + * @returns {ReturnType} + */ + marketplace(): ReturnType { + return marketplace(this); + } + + /** + * Expose the user functions and pass the current instance + * @returns {ReturnType} + */ + user(): ReturnType { + return user(this); + } +} diff --git a/lib/collection.js b/lib/collection.js deleted file mode 100644 index 20bd81d..0000000 --- a/lib/collection.js +++ /dev/null @@ -1,160 +0,0 @@ -'use strict'; - -var util = require('./util.js'), - AuthError = require('./error.js').AuthError; - -module.exports = function(client){ - var collection = {}; - - /** - * Get a list of all collection folders for the given user - * @param {string} user - The user name - * @param {function} callback - The callback - * @returns {DiscogsClient|Promise} - */ - - collection.getFolders = function(user, callback){ - return client.get('/users/'+util.escape(user)+'/collection/folders', callback); - }; - - /** - * Get metadata for a specified collection folder - * @param {string} user - The Discogs user name - * @param {number|string} folder - A folder ID (0 = public folder) - * @param {function} callback - The callback - * @returns {DiscogsClient|Promise} - */ - - collection.getFolder = function(user, folder, callback){ - if(client.authenticated(2) || (parseInt(folder, 10) === 0)){ - return client.get('/users/'+util.escape(user)+'/collection/folders/'+folder, callback); - }else if(typeof callback === 'function'){ - callback(new AuthError()); - return client; - }else{ - return Promise.reject(new AuthError()); - } - }; - - /** - * Add a new collection folder - * @param {string} user - The user name - * @param {string} name - The folder name - * @param {function} [callback] - The callback - * @returns {DiscogsClient|Promise} - */ - - collection.addFolder = function(user, name, callback){ - return client.post({url: '/users/'+util.escape(user)+'/collection/folders', authLevel: 2}, {name: name}, callback); - }; - - /** - * Change a folder name. The name of folder 0 and 1 can't be changed. - * @param {string} user - The user name - * @param {(number|string)} folder - The folder ID - * @param {string} name - The new folder name - * @param {function} [callback] - The callback - * @returns {DiscogsClient|Promise} - */ - - collection.setFolderName = function(user, folder, name, callback){ - return client.post({url: '/users/'+util.escape(user)+'/collection/folders/'+folder, authLevel: 2}, {name: name}, callback); - }; - - /** - * Delete a folder. A folder must be empty before it can be deleted. - * @param {string} user - The user name - * @param {(number|string)} folder - The folder ID - * @param {function} [callback] - The callback - * @returns {DiscogsClient|Promise} - */ - - collection.deleteFolder = function(user, folder, callback){ - return client.delete({url: '/users/'+util.escape(user)+'/collection/folders/'+folder, authLevel: 2}, callback); - }; - - /** - * Get the releases in a user's collection folder (0 = public folder) - * @param {string} user - The user name - * @param {(number|string)} folder - The folder ID - * @param {object} [params] - Optional extra pagination and sorting params, see url above - * @param {function} [callback] - The callback - * @returns {DiscogsClient|Promise} - */ - - collection.getReleases = function(user, folder, params, callback){ - if(client.authenticated(2) || (parseInt(folder, 10) === 0)){ - var path = '/users/'+util.escape(user)+'/collection/folders/'+folder+'/releases'; - if((arguments.length === 3) && (typeof params === 'function')){ - callback = params; - }else{ - path = util.addParams(path, params); - } - return client.get(path, callback); - }else if(typeof callback === 'function'){ - callback(new AuthError()); - return client; - }else{ - return Promise.reject(new AuthError()); - } - }; - - /** - * Get the instances of a release in a user's collection - * @param {string} user - The user name - * @param {(number|string)} release - The release ID - * @param {function} [callback] - The callback - * @returns {DiscogsClient|Promise} - */ - - collection.getReleaseInstances = function(user, release, callback){ - return client.get('/users/'+util.escape(user)+'/collection/releases/'+release, callback); - }; - - /** - * Add a release instance to the (optionally) given collection folder - * @param {string} user - The user name - * @param {(number|string)} [folder] - The folder ID (defaults to the "Uncategorized" folder) - * @param {(number|string)} release - The release ID - * @param {function} [callback] - The callback - * @returns {DiscogsClient|Promise} - */ - - collection.addRelease = function(user, folder, release, callback){ - if((arguments.length === 3) && (typeof release === 'function')){ - callback = release; release = folder; folder = 1; - } - return client.post({url: '/users/'+util.escape(user)+'/collection/folders/'+(folder||1)+'/releases/'+release, authLevel: 2}, null, callback); - }; - - /** - * Edit a release instance in the given collection folder - * @param {string} user - The user name - * @param {(number|string)} folder - The folder ID - * @param {(number|string)} release - The release ID - * @param {(number|string)} instance - The release instance ID - * @param {object} data - The instance data {rating: 4, folder_id: 1532} - * @param {function} [callback] - The callback - * @returns {DiscogsClient|Promise} - */ - - collection.editRelease = function(user, folder, release, instance, data, callback){ - return client.post({url: '/users/'+util.escape(user)+'/collection/folders/'+folder+'/releases/'+release+'/instances/'+instance, authLevel: 2}, data, callback); - }; - - /** - * Delete a release instance from the given folder - * @param {string} user - The user name - * @param {(number|string)} folder - The folder ID - * @param {(number|string)} release - The release ID - * @param {(number|string)} instance - The release instance ID - * @param {function} [callback] - The callback - * @returns {DiscogsClient|Promise} - */ - - collection.removeRelease = function(user, folder, release, instance, callback){ - return client.delete({url: '/users/'+util.escape(user)+'/collection/folders/'+folder+'/releases/'+release+'/instances/'+instance, authLevel: 2}, callback); - }; - - return collection; -}; \ No newline at end of file diff --git a/lib/collection.ts b/lib/collection.ts new file mode 100644 index 0000000..aee41c7 --- /dev/null +++ b/lib/collection.ts @@ -0,0 +1,351 @@ +import { escape, toQueryString } from './util.js'; +import { AuthError } from './error.js'; +import { + type RateLimitedResponse, + type PaginationParameters, + type PaginationResponse, + SortParameters, +} from './types.js'; +import { type DiscogsClient } from './client.js'; + +type BasicReleaseInfo = { + id: number; + title: string; + year: number; + resource_url: string; + thumb: string; + cover_image: string; + formats: Array<{ qty: string; descriptions: Array; name: string }>; + labels: Array<{ resource_url: string; entity_type: string; catno: string; id: number; name: string }>; + artists: Array<{ + id: number; + name: string; + join: string; + resource_url: string; + anv: string; + tracks: string; + role: string; + }>; + genres: Array; + styles: Array; +}; +type GetFoldersResponse = { folders: Array }; +type GetFolderResponse = { id: number; count: number; name: string; resource_url: string }; +type GetReleasesResponse = { + releases: Array<{ + id: number; + instance_id: number; + folder_id: number; + rating: number; + basic_information: BasicReleaseInfo; + notes: Array<{ field_id: number; value: string }>; + }>; +}; +type GetReleaseInstancesResponse = { + releases: Array<{ + id: number; + instance_id: number; + folder_id: number; + rating: number; + basic_information: BasicReleaseInfo; + date_added: string; + }>; +}; +type AddReleaseResponse = { instance_id: number; resource_url: string }; +type GetFieldsResponse = { + fields: Array<{ + name: string; + type: string; + public: boolean; + position: number; + id: number; + options?: Array; + lines?: number; + }>; +}; +type CollectionValueResponse = { maximum: string; median: string; minimum: string }; + +/** + * @param {DiscogsClient} client + */ +export default function (client: DiscogsClient) { + return { + /** + * Get a list of all collection folders for the given user + * @param {string} user - The user name + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-get + * + * @example + * await client.user().collection().getFolders('rodneyfool'); + */ + getFolders: function (user: string): Promise> { + // @ts-ignore + return client.get(`/users/${escape(user)}/collection/folders`); + }, + + /** + * Get metadata for a specified collection folder + * @param {string} user - The Discogs user name + * @param {number | string} folder - A folder ID (0 = public folder) + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-folder-get + * + * @example + * await client.user().collection().getFolder('rodneyfool', 3); + */ + getFolder: function (user: string, folder: number | string): Promise> { + if (client.authenticated(2) || Number(folder) === 0) { + // @ts-ignore + return client.get(`/users/${escape(user)}/collection/folders/${folder}`); + } + + return Promise.reject(new AuthError()); + }, + + /** + * Add a new collection folder + * @param {string} user - The user name + * @param {string} name - The folder name + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-post + * + * @example + * await client.user().collection().addFolder('rodneyfool', 'My favorites'); + */ + addFolder: function (user: string, name: string): Promise> { + // @ts-ignore + return client.post({ url: `/users/${escape(user)}/collection/folders`, authLevel: 2 }, { name: name }); + }, + + /** + * Change a folder name. The name of folder 0 and 1 can't be changed. + * @param {string} user - The user name + * @param {(number|string)} folder - The folder ID + * @param {string} name - The new folder name + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-folder-post + * + * @example + * await client.user().collection().setFolderName('rodneyfool', 3, 'New Name'); + */ + setFolderName: function ( + user: string, + folder: number | string, + name: string + ): Promise> { + // @ts-ignore + return client.post( + { url: `/users/${escape(user)}/collection/folders/${folder}`, authLevel: 2 }, + { name: name } + ); + }, + + /** + * Delete a folder. A folder must be empty before it can be deleted. + * @param {string} user - The user name + * @param {(number|string)} folder - The folder ID + * @returns {Promise>>} + * + * @example + * await client.user().collection().deleteFolder('rodneyfool', 3); + */ + deleteFolder: function (user: string, folder: number | string): Promise> { + // @ts-ignore + return client.delete({ url: `/users/${escape(user)}/collection/folders/${folder}`, authLevel: 2 }); + }, + + /** + * Get the releases in a user's collection folder (0 = public folder) + * @param {string} user - The user name + * @param {(number|string)} folder - The folder ID + * @param {PaginationParameters & SortParameters<'label'|'artist'|'title'|'catno'|'format'|'rating'|'added'|'year'>} [params] - Optional extra pagination and sorting params + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-items-by-folder-get + * + * @example + * await client.user().collection().getReleases('rodneyfool', 3, { sort: 'artist', sort_order: 'desc' }); + */ + getReleases: function ( + user: string, + folder: number | string, + params?: PaginationParameters & + SortParameters<'label' | 'artist' | 'title' | 'catno' | 'format' | 'rating' | 'added' | 'year'> + ): Promise> { + if (client.authenticated(2) || Number(folder) === 0) { + let path = `/users/${escape(user)}/collection/folders/${folder}/releases?${toQueryString(params)}`; + // @ts-ignore + return client.get(path); + } + return Promise.reject(new AuthError()); + }, + + /** + * Get the instances of a release in a user's collection + * @param {string} user - The user name + * @param {(number|string)} release - The release ID + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-items-by-release-get + * + * @example + * await client.user().collection().getReleaseInstances('susan.salkeld', 7781525); + */ + getReleaseInstances: function ( + user: string, + release: number | string + ): Promise> { + // @ts-ignore + return client.get(`/users/${escape(user)}/collection/releases/${release}`); + }, + + /** + * Add a release instance to the (optionally) given collection folder + * @param {string} user - The user name + * @param {(number|string)} release - The release ID + * @param {(number|string)} [folder] - The folder ID (defaults to the "Uncategorized" folder) + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-add-to-collection-folder-post + * + * @example + * await client.user().collection().addRelease('rodneyfool', 3, 130076); + */ + addRelease: function ( + user: string, + release: number | string, + folder: number | string = 1 + ): Promise> { + // @ts-ignore + return client.post( + { url: `/users/${escape(user)}/collection/folders/${folder}/releases/${release}`, authLevel: 2 }, + undefined + ); + }, + + /** + * Edit a release instance in the given collection folder + * @param {string} user - The user name + * @param {(number|string)} folder - The folder ID + * @param {(number|string)} release - The release ID + * @param {(number|string)} instance - The release instance ID + * @param {Partial<{ rating: 1 | 2 | 3 | 4 | 5 | null; folder_id: number }>} data - The instance data + * @returns {Promise>>} + * + * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-change-rating-of-release-post + * + * @example + * await client.user().collection().editRelease('rodneyfool', 4, 130076, 1, { rating: 5, folder_id: 16 }); + */ + editRelease: function ( + user: string, + folder: number | string, + release: number | string, + instance: number | string, + data: Partial<{ rating: 1 | 2 | 3 | 4 | 5 | null; folder_id: number }> + ): Promise> { + // @ts-ignore + return client.post( + { + url: `/users/${escape( + user + )}/collection/folders/${folder}/releases/${release}/instances/${instance}`, + authLevel: 2, + }, + data + ); + }, + + /** + * Remove an instance of a release from a user's collection folder. + * @param {string} user - The user name + * @param {(number|string)} folder - The folder ID + * @param {(number|string)} release - The release ID + * @param {(number|string)} instance - The release instance ID + * @returns {Promise>>} + * + * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-delete-instance-from-folder-delete + * + * @example + * await client.user().collection().removeRelease('rodneyfool', 3, 130076, 1); + */ + removeRelease: function ( + user: string, + folder: number | string, + release: number | string, + instance: number | string + ): Promise> { + // @ts-ignore + return client.delete({ + url: `/users/${escape(user)}/collection/folders/${folder}/releases/${release}/instances/${instance}`, + authLevel: 2, + }); + }, + + /** + * Retrieve a list of user-defined collection notes fields. + * These fields are available on every release in the collection. + * @param {string} user - The user name + * @returns {Promise>>} + * + * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-list-custom-fields-get + * + * @example + * await client.user().collection().getFields('rodneyfool'); + */ + getFields: function (user: string): Promise> { + // @ts-ignore + return client.get(`/users/${escape(user)}/collection/fields`); + }, + + /** + * Change the value of a notes field on a particular instance. + * @param {string} user - The user name + * @param {(number|string)} folder - The folder ID + * @param {(number|string)} release - The release ID + * @param {(number|string)} instance - The release instance ID + * @param {number} field - The ID of the field + * @param {string} value - The new value of the field + * @returns {Promise>>} + * + * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-edit-fields-instance-post + * + * @example + * await client.user().collection().editInstanceNote('rodneyfool', 3, 130076, 1, 8, 'foo'); + */ + editInstanceNote: function ( + user: string, + folder: number | string, + release: number | string, + instance: number | string, + field: number, + value: string + ): Promise> { + let path = `/users/${escape( + user + )}/collection/folders/${folder}/releases/${release}/instances/${instance}/fields/${field}?value=${value}`; + // @ts-ignore + return client.post(path, null); + }, + + /** + * Returns the minimum, median, and maximum value of a user's collection + * @param {string} user - The user name + * @returns {Promise>>} + * + * @see https://www.discogs.com/developers/#page:user-collection,header:user-collection-collection-value-get + * + * @example + * await client.user().collection().getValue('rodneyfool'); + */ + getValue: function (user: string): Promise> { + // @ts-ignore + return client.get({ url: `/users/${escape(user)}/collection/value`, authLevel: 2 }); + }, + }; +} diff --git a/lib/database.js b/lib/database.js deleted file mode 100644 index 4f16a6c..0000000 --- a/lib/database.js +++ /dev/null @@ -1,179 +0,0 @@ -'use strict'; - -var util = require('./util.js'); - -module.exports = function(client) { - var database = {}; - - /** - * Expose Discogs database status constants - */ - - database.status = {accepted: 'Accepted', draft: 'Draft', deleted: 'Deleted', rejected: 'Rejected'}; - - /** - * Get artist data - * @param {(number|string)} artist - The Discogs artist ID - * @param {object} [options] - Show releases by the artist + pagination params - * @param {function} [callback] - Callback function - * @return {DiscogsClient|Promise} - */ - - database.getArtist = function(artist, callback) { - return client.get('/artists/' + artist, callback); - }; - - /** - * Get artist release data - * @param {(number|string)} artist - The Discogs artist ID - * @param {object} [params] - Optional pagination params - * @param {function} [callback] - Callback function - * @return {DiscogsClient|Promise} - */ - - database.getArtistReleases = function(artist, params, callback) { - var path = '/artists/' + artist + '/releases'; - if ((arguments.length === 2) && (typeof params === 'function')) { - callback = params; - } else { - path = util.addParams(path, params); - } - return client.get(path, callback); - }; - - /** - * Get release data - * @param {(number|string)} release - The Discogs release ID - * @param {function} [callback] - Callback - * @return {DiscogsClient|Promise} - */ - - database.getRelease = function(release, callback) { - return client.get('/releases/' + release, callback); - }; - - /** - * Get the release rating for the given user - * @param {(number|string)} release - The Discogs release ID - * @param {string} user - The Discogs user name - * @param {function} [callback] - Callback function - * @return {DiscogsClient|Promise} - */ - - database.getReleaseRating = function(release, user, callback) { - return client.get('/releases/' + release + '/rating/' + util.escape(user), callback); - }; - - /** - * Set (or remove) a release rating for the given logged in user - * @param {(number|string)} release - The Discogs release ID - * @param {string} user - The Discogs user name - * @param {number} rating - The new rating for a release between 1 and 5. Null = remove rating - * @param {function} [callback] - Callback function - * @return {DiscogsClient|Promise} - */ - - database.setReleaseRating = function(release, user, rating, callback) { - var url = '/releases/' + release + '/rating/' + util.escape(user); - if (!rating) { - return client.delete({url: url, authLevel: 2}, callback); - } else { - return client.put({url: url, authLevel: 2}, {rating: ((rating > 5) ? 5 : rating)}, callback); - } - }; - - /** - * Get master release data - * @param {(number|string)} master - The Discogs master release ID - * @param {function} [callback] - Callback function - * @return {DiscogsClient|Promise} - */ - - database.getMaster = function(master, callback) { - return client.get('/masters/' + master, callback); - }; - - /** - * Get the release versions contained in the given master release - * @param {(number|string)} master - The Discogs master release ID - * @param {object} [params] - optional pagination params - * @param {function} [callback] - Callback function - * @return {DiscogsClient|Promise} - */ - - database.getMasterVersions = function(master, params, callback) { - var path = '/masters/' + master + '/versions'; - if ((arguments.length === 2) && (typeof params === 'function')) { - callback = params; - } else { - path = util.addParams(path, params); - } - return client.get(path, callback); - }; - - /** - * Get label data - * @param {(number|string)} label - The Discogs label ID - * @param {function} [callback] - Callback function - * @return {DiscogsClient|Promise} - */ - - database.getLabel = function(label, callback) { - return client.get('/labels/' + label, callback); - }; - - /** - * Get label release data - * @param {(number|string)} label - The Discogs label ID - * @param {object} [params] - Optional pagination params - * @param {function} [callback] - Callback function - * @return {DiscogsClient|Promise} - */ - - database.getLabelReleases = function(label, params, callback) { - var path = '/labels/' + label + '/releases'; - if ((arguments.length === 2) && (typeof params === 'function')) { - callback = params; - } else { - path = util.addParams(path, params); - } - return client.get(path, callback); - }; - - /** - * Get an image - * @param {string} url - The full image url - * @param {function} [callback] - Callback function - * @return {DiscogsClient|Promise} - */ - - database.getImage = function(url, callback) { - return client.get({url: url, encoding: 'binary', queue: false, json: false}, callback); - }; - - /** - * Search the database - * @param {string} query - The search query - * @param {object} [params] - Search parameters as defined on http://www.discogs.com/developers/#page:database,header:database-search - * @param {function} [callback] - Callback function - * @return {DiscogsClient|Promise} - */ - - database.search = function(query, params, callback) { - var obj = {}; - if ((arguments.length === 2) && (typeof params === 'function')) { - callback = params; - } - if (typeof params === 'object') { - obj = params; - } else if (typeof query === 'object') { - obj = query; - } - if (typeof query === 'string') { - obj.q = query; - } - return client.get({url: util.addParams('/database/search', obj), authLevel: 1}, callback); - }; - - return database; -}; \ No newline at end of file diff --git a/lib/database.ts b/lib/database.ts new file mode 100644 index 0000000..3e64358 --- /dev/null +++ b/lib/database.ts @@ -0,0 +1,471 @@ +import { type DiscogsClient } from './client.js'; +import { + type Image, + type GetReleaseResponse, + type RateLimitedResponse, + type Artist, + type Tracklisting, + type PaginationParameters, + type Currency, + SortParameters, + PaginationResponse, +} from './types.js'; +import { toQueryString, escape } from './util.js'; +type GetArtistResponse = { + namevariations: Array; + profile: string; + releases_url: string; + resource_url: string; + uri: string; + urls: Array; + data_quality: string; + id: number; + images: Array; + members: Array<{ active: boolean; id: number; name: string; resource_url: string }>; +}; +type GetArtistReleasesResponses = { + releases: Array<{ + artist: string; + id: number; + main_release: number; + resource_url: string; + role: string; + thumb: string; + title: string; + type: string; + year: number; + }>; +}; +type GetReleaseRatingResponse = { username: string; release_id: number; rating: number }; +type GetReleaseCommunityRatingResponse = { rating: { count: number; average: number }; release_id: number }; +type GetReleaseStatsResponse = { num_have: number; num_want: number }; +type GetMasterResponse = { + styles: Array; + genres: Array; + videos: Array<{ duration: number; description: string; embed: boolean; uri: string; title: string }>; + title: string; + main_release: number; + main_release_url: string; + uri: string; + artists: Array; + versions_url: string; + year: number; + images: Array; + resource_url: string; + tracklist: Array; + id: number; + num_for_sale: number; + lowest_price: number; + data_quality: string; +}; +type GetMasterVersionsResponse = { + versions: Array<{ + status: string; + stats: { + user: { in_collection: number; in_wantlist: number }; + community: { in_collection: number; in_wantlist: number }; + }; + thumb: string; + format: string; + country: string; + title: string; + label: string; + released: string; + major_formats: Array; + catno: string; + resource_url: string; + id: number; + }>; +}; +type GetLabelResponse = { + profile: string; + releases_url: string; + name: string; + contact_info: string; + uri: string; + sublabels: Array<{ resource_url: string; id: number; name: string }>; + urls: Array; + images: Array; + resource_url: string; + id: number; + data_quality: string; +}; +type GetLabelReleasesResponse = { + releases: Array<{ + artist: string; + catno: string; + format: string; + id: number; + resource_url: string; + status: string; + thumb: string; + title: string; + year: number; + }>; +}; +interface SearchResult { + id: number; + type: string; + user_data: UserData; + master_id?: number; + master_url?: string; + uri: string; + title: string; + thumb: string; + cover_image: string; + resource_url: string; + country?: string; + year?: string; + format?: string[]; + label?: string[]; + genre?: string[]; + style?: string[]; + barcode?: string[]; + catno?: string; + community?: Community; + format_quantity?: number; + formats?: Format[]; +} + +interface UserData { + in_wantlist: boolean; + in_collection: boolean; +} + +interface Community { + want: number; + have: number; +} + +interface Format { + name: string; + qty: string; + descriptions?: string[]; +} + +type SearchResponse = { + results: Array; +}; +type SearchParameters = { + query: string; + type: 'release' | 'master' | 'artist' | 'label'; + title: string; + release_title: string; + credit: string; + artist: string; + anv: string; + label: string; + genre: string; + style: string; + country: string; + year: string; + format: string; + catno: string; + barcode: string; + track: string; + submitter: string; + contributor: string; +}; + +/** + * @param {DiscogsClient} client + * @see https://www.discogs.com/developers/#page:database + */ +export default function (client: DiscogsClient) { + return { + /** + * @TODO possible turn this into an enum and use it in 'status' type definitions instead of `status: string` + * Expose Discogs database status constants + */ + status: { accepted: 'Accepted', draft: 'Draft', deleted: 'Deleted', rejected: 'Rejected' }, + + /** + * Get artist data + * @param {(number|string)} artist - The Discogs artist ID + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:database,header:database-artist-get + * + * @example + * await client.database().getArtist(108713); + */ + getArtist: function (artist: number | string): Promise> { + // @ts-ignore + return client.get('/artists/' + artist); + }, + + /** + * Get artist release data + * @param {(number|string)} artist - The Discogs artist ID + * @param {PaginationParameters & SortParameters<'year'|'title'|'format'>} [params] - Optional pagination params + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:database,header:database-artist-releases-get + * + * @example + * await client.database().getArtistReleases(108713, { page: 2, sort: 'year', sort_order: 'asc' }); + */ + getArtistReleases: function ( + artist: number | string, + params?: PaginationParameters & SortParameters<'year' | 'title' | 'format'> + ): Promise> { + let path = `/artists/${artist}/releases?${toQueryString(params)}`; + // @ts-ignore + return client.get(path); + }, + + /** + * Get release data + * @param {(number|string)} release - The Discogs release ID + * @param {Currency} [currency] - Currency for marketplace data. Defaults to the authenticated users currency. + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:database,header:database-release-get + * + * @example + * await client.database().getRelease(249504); + * + * @example + * await client.database().getRelease(249504, 'USD'); + */ + getRelease: function ( + release: number | string, + currency?: Currency + ): Promise> { + let path = `/releases/${release}`; + if (currency !== undefined) { + path += `?${toQueryString({ curr_abbr: currency })}`; + } + // @ts-ignore + return client.get(path); + }, + + /** + * Get the release rating for the given user + * @param {(number|string)} release - The Discogs release ID + * @param {string} user - The Discogs user name + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:database,header:database-release-rating-by-user-get + * + * @example + * await client.database().getReleaseRating(249504, 'rodneyfool'); + */ + getReleaseRating: function ( + release: number | string, + user: string + ): Promise> { + // @ts-ignore + return client.get(`/releases/${release}/rating/${escape(user)}`); + }, + + /** + * Set (or remove) a release rating for the given logged in user + * @param {(number|string)} release - The Discogs release ID + * @param {string} user - The Discogs user name + * @param {1 | 2 | 3 | 4 | 5 | null} rating - The new rating for a release between 1 and 5. Null = remove rating + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:database,header:database-release-rating-by-user-put + * @see https://www.discogs.com/developers/#page:database,header:database-release-rating-by-user-delete + * + * @example + * await client.database().setReleaseRating(249504, 'rodneyfool', 2); + * + * @example + * await client.database().setReleaseRating(249504, 'rodneyfool', null); + */ + setReleaseRating: function ( + release: number | string, + user: string, + rating: 1 | 2 | 3 | 4 | 5 | null + ): Promise> { + let url = `/releases/${release}/rating/${escape(user)}`; + if (!rating) { + // @ts-ignore + return client.delete({ url: url, authLevel: 2 }); + } else { + // @ts-ignore + return client.put({ url: url, authLevel: 2 }, { rating: rating > 5 ? 5 : rating }); + } + }, + + /** + * Get the average rating and the total number of user ratings for a given release. + * @param {(number|string)} release - The Discogs release ID + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:database,header:database-community-release-rating-get + * + * @example + * await client.database().getReleaseCommunityRating(249504); + */ + getReleaseCommunityRating: function ( + release: number | string + ): Promise> { + let path = `/releases/${release}/rating`; + // @ts-ignore + return client.get(path); + }, + + /** + * Get the total number of "haves" (in the community's collections) + * and "wants" (in the community's wantlists) for a given release. + * @param {(number|string)} release - The Discogs release ID + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:database,header:database-release-stats-get + * + * @example + * await client.database().getReleaseStats(249504); + */ + getReleaseStats: function (release: number | string): Promise> { + let path = `/releases/${release}/stats`; + // @ts-ignore + return client.get(path); + }, + + /** + * Get master release data + * @param {(number|string)} master - The Discogs master release ID + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:database,header:database-master-release-get + * + * @example + * await client.database().getMaster(1000); + */ + getMaster: function (master: number | string): Promise> { + // @ts-ignore + return client.get(`/masters/${master}`); + }, + + /** + * Get the release versions contained in the given master release + * @param {(number|string)} master - The Discogs master release ID + * @param {PaginationParameters & Partial<{ format: string; label: string; released: string; country: string } & SortParameters<'released'|'title'|'format'|'label'|'catno'|'country'>>} [params] - optional pagination params + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:database,header:database-master-release-versions-get + * + * @example + * await client.database().getMasterVersions(1000, { + * page: 2, + * per_page: 25, + * format: 'Vinyl', + * label: 'Scorpio Music', + * released: '1992', + * country: 'Belgium', + * sort: 'released', + * sort_order: 'asc' + * }); + */ + getMasterVersions: function ( + master: number | string, + params?: PaginationParameters & + Partial< + { format: string; label: string; released: string; country: string } & SortParameters< + 'released' | 'title' | 'format' | 'label' | 'catno' | 'country' + > + > + ): Promise> { + let path = `/masters/${master}/versions?${toQueryString(params)}`; + // @ts-ignore + return client.get(path); + }, + + /** + * Get label data + * @param {(number|string)} label - The Discogs label ID + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:database,header:database-label-get + * + * @example + * await client.database().getLabel(1) + */ + getLabel: function (label: number | string): Promise> { + // @ts-ignore + return client.get(`/labels/${label}`); + }, + + /** + * Get label release data + * @param {(number|string)} label - The Discogs label ID + * @param {PaginationParameters} [params] - Optional pagination params + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:database,header:database-all-label-releases-get + * + * @example + * await client.database().getLabelReleases(1, { page: 3, per_page: 25 }); + */ + getLabelReleases: function ( + label: number | string, + params?: PaginationParameters + ): Promise> { + let path = `/labels/${label}/releases?${toQueryString(params)}`; + // @ts-ignore + return client.get(path); + }, + + /** + * Get an image + * @param {string} url - The full image url + * @returns {Promise<{data: string}>} + * + * @see https://www.discogs.com/developers/#page:images + * + * @example + * let { data: release } = await client.database().getRelease(176126); + * let { data: imageData } = await client.database().getImage(release.images[0].resource_url); + */ + getImage: function (url: string): Promise<{ data: string }> { + // @ts-ignore + return client.get({ url: url, encoding: 'binary', queue: false, json: false }); + }, + + /** + * Search the database + * @param {PaginationParameters & Partial} [params] - Search parameters + * + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:database,header:database-search-get + * + * @example + * await client.database().search({ + * query: 'nirvana', // Your search query + * type: 'release', // One of 'release', 'master', 'artist', 'label' + * title: 'nirvana - nevermind', // Search by combined “Artist Name - Release Title” title field. + * release_title: 'nevermind', // Search release titles. + * credit: 'kurt', // Search release credits. + * artist: 'nirvana', // Search artist names. + * anv: 'nirvana', // Search artist ANV. + * label: 'dgc', // Search label names. + * genre: 'rock', // Search genres. + * style: 'grunge', // Search styles. + * country: 'canada', // Search release country. + * year: '1991', // Search release year. + * format: 'album', // Search formats. + * catno: 'DGCD-24425', // Search catalog number. + * barcode: '7 2064-24425-2 4', // Search barcodes. + * track: 'smells like teen spirit', // Search track titles. + * submitter: 'milKt', // Search submitter username. + * contributor: 'jerome99', // Search contributor usernames. + * }); + */ + search: function ( + params: PaginationParameters & Partial = {} + ): Promise> { + let args = { ...params }; + if (args.query) { + // @ts-ignore + args.q = args.query; + delete args.query; + } + // @ts-ignore + return client.get({ url: `/database/search?${toQueryString(args)}`, authLevel: 1 }); + }, + }; +} diff --git a/lib/error.js b/lib/error.js deleted file mode 100644 index 70fb184..0000000 --- a/lib/error.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -var Errors = module.exports = {}; - -/** - * Discogs generic error - * @param {number} [statusCode] - A HTTP status code - * @param {string} [message] - The error message - * @returns {DiscogsError} - */ - -function DiscogsError(statusCode, message){ - Error.captureStackTrace(this, this.constructor); - this.statusCode = statusCode||404; - this.message = message||'Unknown error.'; -} -DiscogsError.prototype = Object.create(Error.prototype, { - constructor: {value: DiscogsError}, - name: {value: 'DiscogsError'}, - toString: {value: function(){ - return this.name+': '+this.statusCode+' '+this.message; - }} -}); -Errors.DiscogsError = DiscogsError; - -/** - * Discogs authorization error - * @returns {AuthError} - */ - -function AuthError(){ - Error.captureStackTrace(this, this.constructor); -} -AuthError.prototype = Object.create(DiscogsError.prototype, { - constructor: {value: AuthError}, - name: {value: 'AuthError'}, - message: {value: 'You must authenticate to access this resource.'}, - statusCode: {value: 401} -}); -Errors.AuthError = AuthError; \ No newline at end of file diff --git a/lib/error.ts b/lib/error.ts new file mode 100644 index 0000000..f9caba1 --- /dev/null +++ b/lib/error.ts @@ -0,0 +1,34 @@ +export class DiscogsError extends Error { + statusCode: number; + + /** + * Discogs generic error + * @param {number} [statusCode] - A HTTP status code + * @param {string} [message] - The error message + */ + constructor(statusCode?: number, message?: string) { + super(message || 'Unknown error.'); + this.name = this.constructor.name; + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor); + } + this.statusCode = statusCode || 404; + } + + toString() { + return this.name + ': ' + this.statusCode + ' ' + this.message; + } +} + +/** + * Discogs authorization error + */ +export class AuthError extends DiscogsError { + constructor() { + super(401, 'You must authenticate to access this resource.'); + this.name = this.constructor.name; + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor); + } + } +} diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..7e2a076 --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,9 @@ +/** + * Expose Discogs utility function library + */ +export * from './util.js'; + +/** + * Expose Discogs Client class + */ +export { DiscogsClient } from './client.js'; diff --git a/lib/list.js b/lib/list.js deleted file mode 100644 index fd78dae..0000000 --- a/lib/list.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -var util = require('./util.js'); - -module.exports = function(client){ - var list = {}; - - /** - * Get the items in a list by list ID - * @param {(number|string)} list - The list ID - * @param {object} [params] - Optional pagination params - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - list.getItems = function(list, params, callback){ - var path = '/lists/'+util.escape(list); - if((arguments.length === 2) && (typeof params === 'function')){ - callback = params; - }else{ - path = util.addParams(path, params); - } - return client.get(path, callback); - }; - - return list; -}; \ No newline at end of file diff --git a/lib/list.ts b/lib/list.ts new file mode 100644 index 0000000..ed58068 --- /dev/null +++ b/lib/list.ts @@ -0,0 +1,46 @@ +import { type DiscogsClient } from './client.js'; +import { RateLimitedResponse } from './types.js'; +import { escape } from './util.js'; + +type GetListItemsResponse = { + created_ts: string; + modified_ts: string; + name: string; + list_id: number; + url: string; + items: Array<{ + comment: string; + display_title: string; + uri: string; + image_url: string; + resource_url: string; + type: string; + id: number; + }>; + resource_url: string; + public: boolean; + description: string; +}; + +/** + * @param {DiscogsClient} client + */ +export default function (client: DiscogsClient) { + return { + /** + * Get the items in a list by list ID + * @param {(number|string)} list - The list ID + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:user-lists,header:user-lists-list-get + * + * @example + * await client.user().list().getItems(123) + */ + getItems: function (list: number | string): Promise> { + let path = `/lists/${escape(list.toString())}`; + // @ts-ignore + return client.get(path); + }, + }; +} diff --git a/lib/marketplace.js b/lib/marketplace.js deleted file mode 100644 index dc69966..0000000 --- a/lib/marketplace.js +++ /dev/null @@ -1,159 +0,0 @@ -'use strict'; - -var util = require('./util.js'); - -module.exports = function(client){ - var marketplace = {}; - - /** - * Copy the getInventory function from the user module - */ - - marketplace.getInventory = require('./user.js')(client).getInventory; - - /** - * Get a marketplace listing - * @param {(number|string)} listing - The listing ID - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - marketplace.getListing = function(listing, callback){ - return client.get('/marketplace/listings/'+listing, callback); - }; - - /** - * Create a marketplace listing - * @param {object} data - The data for the listing - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - marketplace.addListing = function(data, callback){ - return client.post({url: '/marketplace/listings', authLevel: 2}, data, callback); - }; - - /** - * Edit a marketplace listing - * @param {(number|string)} listing - The listing ID - * @param {object} data - The data for the listing - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - marketplace.editListing = function(listing, data, callback){ - return client.post({url: '/marketplace/listings/'+listing, authLevel: 2}, data, callback); - }; - - /** - * Delete a marketplace listing - * @param {(number|string)} [listing] - The listing ID - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - marketplace.deleteListing = function(listing, callback){ - return client.delete({url: '/marketplace/listings/'+listing, authLevel: 2}, callback); - }; - - /** - * Get a list of the authenticated user's orders - * @param {object} [params] - Optional sorting and pagination params - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - marketplace.getOrders = function(params, callback){ - var path = '/marketplace/orders'; - if((arguments.length === 1) && (typeof params === 'function')){ - callback = params; - }else if(typeof params === 'object'){ - path = util.addParams(path, params); - } - return client.get({url: path, authLevel: 2}, callback); - }; - - /** - * Get details of a marketplace order - * @param {string} order - The order ID - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - marketplace.getOrder = function(order, callback){ - return client.get({url: '/marketplace/orders/'+order, authLevel: 2}, callback); - }; - - /** - * Edit a marketplace order - * @param {string} order - The order ID - * @param {object} data - The data for the order - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - marketplace.editOrder = function(order, data, callback){ - return client.post({url: '/marketplace/orders/'+order, authLevel: 2}, data, callback); - }; - - /** - * List the messages for the given order ID - * @param {string} order - The order ID - * @param {object} [params] - Optional pagination parameters - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - marketplace.getOrderMessages = function(order, params, callback){ - var path = '/marketplace/orders/'+order+'/messages'; - if((arguments.length === 2) && (typeof params === 'function')){ - callback = params; - }else{ - path = util.addParams(path, params); - } - return client.get({url: path, authLevel: 2}, callback); - }; - - /** - * Add a message to the given order ID - * @param {string} order - The order ID - * @param {object} data - The message data - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - marketplace.addOrderMessage = function(order, data, callback){ - return client.post({url: '/marketplace/orders/'+order+'/messages', authLevel: 2}, data, callback); - }; - - /** - * Get the marketplace fee for a given price - * @param {(number|string)} price - The price as a number or string - * @param {string} [currency] - Optional currency as one of USD, GBP, EUR, CAD, AUD, or JPY. Defaults to USD. - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - marketplace.getFee = function(price, currency, callback){ - var path = '/marketplace/fee/'+((typeof price === 'number') ? price.toFixed(2) : price); - if((arguments.length === 2) && (typeof currency === 'function')){ - callback = currency; - }else if(currency){ // Get the fee in a given currency - path += '/'+currency; - } - return client.get(path, callback); - }; - - /** - * Get price suggestions for a given release ID in the user's selling currency - * @param {(number|string)} release - The release ID - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - marketplace.getPriceSuggestions = function(release, callback){ - return client.get({url: '/marketplace/price_suggestions/'+release, authLevel: 2}, callback); - }; - - return marketplace; -}; \ No newline at end of file diff --git a/lib/marketplace.ts b/lib/marketplace.ts new file mode 100644 index 0000000..69fadfa --- /dev/null +++ b/lib/marketplace.ts @@ -0,0 +1,367 @@ +import { type DiscogsClient } from './client.js'; +import { + type RateLimitedResponse, + type SortParameters, + type Currency, + type PaginationParameters, + type PaginationResponse, + type Price, + type Listing, +} from './types.js'; +import user from './user.js'; +import { toQueryString } from './util.js'; + +type Condition = + | 'Mint (M)' + | 'Near Mint (NM or M-)' + | 'Very Good Plus (VG+)' + | 'Very Good (VG)' + | 'Good Plus (G+)' + | 'Good (G)' + | 'Fair (F)' + | 'Poor (P)'; +type SleeveCondition = Condition | 'Generic' | 'Not Graded' | 'No Cover'; +type Status = 'Draft' | 'For Sale' | 'Expired'; +type OrderStatus = + | 'New Order' + | 'Buyer Contacted' + | 'Invoice Sent' + | 'Payment Pending' + | 'Payment Received' + | 'Shipped' + | 'Refund Sent' + | 'Cancelled (Non-Paying Buyer)' + | 'Cancelled (Item Unavailable)' + | "Cancelled (Per Buyer's Request)"; +type Order = { resource_url: string; id: number }; +type OrderMessage = { + timestamp: string; + message: string; + type: string; + order: Order; + subject: string; + refund?: { amount: number; order: Order }; + from?: { id: number; username: string; avatar_url: string; resource_url: string }; + status_id?: number; + actor?: { username: string; resource_url: string }; + original?: number; + new?: number; +}; +type AddListingResponse = { listing_id: number; resource_url: string }; +type GetOrderResponse = { + id: number; + resource_url: string; + messages_url: string; + uri: string; + status: OrderStatus; + next_status: Array; + fee: Price; + created: string; + items: Array<{ + release: { id: number; description: string }; + price: Price; + media_condition: Condition; + sleeve_condition: SleeveCondition; + id: number; + }>; + shipping: { currency: Currency; method: string; value: number }; + shipping_address: string; + additional_instructions: string; + archived: boolean; + seller: { resource_url: string; username: string; id: number }; + last_activity: string; + buyer: { resource_url: string; username: string; id: number }; + total: Price; +}; +type GetReleaseStatsResponse = { lowest_price?: Price; num_for_sale?: number; blocked_from_sale: boolean }; + +/** + * @param {DiscogsClient} client + */ +export default function (client: DiscogsClient) { + return { + /** + * Copy the getInventory function from the user module + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-inventory-get + */ + getInventory: user(client).getInventory, + + /** + * Get a marketplace listing + * @param {number} listing - The listing ID + * @param {Currency} [currency] - Optional currency + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-listing-get + * + * @example + * await client.marketplace().getListing(172723812, 'USD'); + */ + getListing: function (listing: number, currency?: Currency): Promise> { + let path = `/marketplace/listings/${listing}`; + if (currency !== undefined) { + path += `?${toQueryString({ curr_abbr: currency })}`; + } + // @ts-ignore + return client.get(path); + }, + + /** + * Create a marketplace listing + * @param {{release_id: number; condition: Condition; price: number; status: Status;} & Partial<{sleeve_condition: SleeveCondition; comments: string; allow_offers: boolean; external_id: string; location: string; weight: number | 'auto'; format_quantity: number | 'auto'}>} data - The data for the listing + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-new-listing-post + * + * @example + * await client.marketplace().addListing({ + * release_id: 1, + * condition: 'Mint (M)', + * sleeve_condition: 'Fair (F)', + * price: 10, + * comments: 'This item is wonderful', + * allow_offers: true, + * status: 'Draft', + * external_id: '1234321', + * location: 'top shelf', + * weight: 200, + * format_quantity: 'auto', + * }); + */ + addListing: function ( + data: { release_id: number; condition: Condition; price: number; status: Status } & Partial<{ + sleeve_condition: SleeveCondition; + comments: string; + allow_offers: boolean; + external_id: string; + location: string; + weight: number | 'auto'; + format_quantity: number | 'auto'; + }> + ): Promise> { + // @ts-ignore + return client.post({ url: '/marketplace/listings', authLevel: 2 }, data); + }, + + /** + * Edit a marketplace listing + * @param {number} listing - The listing ID + * @param {{release_id: number; condition: Condition; price: number; status: Status} & Partial<{sleeve_condition: SleeveCondition; comments: string; allow_offers: boolean; external_id: string; location: string; weight: number | 'auto'; format_quantity: number | 'auto'}>} data - The data for the listing + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-listing-post + * + * @example + * await client.marketplace().editListing(172723812, { + * release_id: 1, + * condition: 'Mint (M)', + * sleeve_condition: 'Fair (F)', + * price: 10, + * comments: 'This item is wonderful', + * allow_offers: true, + * status: 'Draft', + * external_id: '1234321', + * location: 'top shelf', + * weight: 200, + * format_quantity: 'auto', + * }); + */ + editListing: function ( + listing: number, + data: { release_id: number; condition: Condition; price: number; status: Status } & Partial<{ + sleeve_condition: SleeveCondition; + comments: string; + allow_offers: boolean; + external_id: string; + location: string; + weight: number | 'auto'; + format_quantity: number | 'auto'; + }> + ): Promise> { + // @ts-ignore + return client.post({ url: `/marketplace/listings/${listing}`, authLevel: 2 }, data); + }, + + /** + * Delete a marketplace listing + * @param {number} listing - The listing ID + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-listing-delete + * + * @example + * await client.marketplace().deleteListing(172723812); + */ + deleteListing: function (listing: number): Promise> { + // @ts-ignore + return client.delete({ url: `/marketplace/listings/${listing}`, authLevel: 2 }); + }, + + /** + * Get a list of the authenticated user's orders + * @param {Partial<{status: OrderStatus; created_after: string; created_before: string; archived: boolean}> & PaginationParameters & SortParameters<'id' | 'buyer' | 'created' | 'status' | 'last_activity'>} [params] - Optional sorting and pagination params + * @returns {Promise}>>} + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-list-orders-get + * + * @example + * await client.marketplace().getOrders({ + * status: "Cancelled (Per Buyer's Request)", + * created_after: '2019-06-24T20:58:58Z', + * created_before: '2019-06-25T20:58:58Z', + * archived: true, + * sort: 'last_activity', + * sort_order: 'desc', + * page: 2, + * per_page: 50, + * }); + */ + getOrders: function ( + params?: Partial<{ status: OrderStatus; created_after: string; created_before: string; archived: boolean }> & + PaginationParameters & + SortParameters<'id' | 'buyer' | 'created' | 'status' | 'last_activity'> + ): Promise }>> { + let path = `/marketplace/orders?${toQueryString(params)}`; + // @ts-ignore + return client.get({ url: path, authLevel: 2 }); + }, + + /** + * Get details of a marketplace order + * @param {number} order - The order ID + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-order-get + * + * @example + * await client.marketplace().getOrder(1); + */ + getOrder: function (order: number): Promise> { + // @ts-ignore + return client.get({ url: `/marketplace/orders/${order}`, authLevel: 2 }); + }, + + /** + * Edit a marketplace order + * @param {number} order - The order ID + * @param {Partial<{status: OrderStatus; shipping: number}>} data - The data for the order + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-order-post + * + * @example + * await client.marketplace().editOrder(1, { status: 'Shipped', shipping: 10 }); + */ + editOrder: function ( + order: number, + data: Partial<{ status: OrderStatus; shipping: number }> + ): Promise> { + // @ts-ignore + return client.post({ url: `/marketplace/orders/${order}`, authLevel: 2 }, data); + }, + + /** + * List the messages for the given order ID + * @param {number} order - The order ID + * @param {PaginationParameters} [params] - Optional pagination parameters + * @returns {Promise}>>} + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-list-order-messages-get + * + * @example + * await client.marketplace().getOrderMessages(1, { page: 2, per_page: 50 }); + */ + getOrderMessages: function ( + order: number, + params?: PaginationParameters + ): Promise }>> { + let path = `/marketplace/orders/${order}/messages?${toQueryString(params)}`; + // @ts-ignore + return client.get({ url: path, authLevel: 2 }); + }, + + /** + * Add a message to the given order ID + * @param {number} order - The order ID + * @param {Partial<{message: string; status: OrderStatus}>} data - The message data + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-list-order-messages-post + * + * @example + * await client.marketplace().addOrderMessage(1, { message: 'hello world', status: 'New Order' }); + */ + addOrderMessage: function ( + order: number, + data: Partial<{ message: string; status: OrderStatus }> + ): Promise> { + // @ts-ignore + return client.post({ url: '/marketplace/orders/' + order + '/messages', authLevel: 2 }, data); + }, + + /** + * Get the marketplace fee for a given price + * @param {number} price - The price as a number + * @param {Currency} [currency] - Optional currency as one of USD, GBP, EUR, CAD, AUD, or JPY. Defaults to USD. + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-fee-get + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-fee-with-currency-get + * + * @example + * await client.marketplace().getFee(10); + * await client.marketplace().getFee(10, 'EUR'); + */ + getFee: function (price: number, currency?: Currency): Promise> { + let path = `/marketplace/fee/${price.toFixed(2)}`; + if (currency) { + // Get the fee in a given currency + path += '/' + currency; + } + // @ts-ignore + return client.get(path); + }, + + /** + * Get price suggestions for a given release ID in the user's selling currency + * @param {number} release - The release ID + * @returns {Promise>>} + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-price-suggestions-get + * + * @example + * await client.marketplace().getPriceSuggestions(10); + */ + getPriceSuggestions: function (release: number): Promise>> { + // @ts-ignore + return client.get({ url: `/marketplace/price_suggestions/${release}`, authLevel: 2 }); + }, + + /** + * Retrieve marketplace statistics for the provided Release ID. + * These statistics reflect the state of the release in the marketplace currently, + * and include the number of items currently for sale, lowest listed price of any item for sale, + * and whether the item is blocked for sale in the marketplace. + * @param {number} release + * @param {Currency} [currency] + * @returns {Promise>} + * + * @see https://www.discogs.com/developers/#page:marketplace,header:marketplace-release-statistics-get + * + * @example + * await client.marketplace().getReleaseStats(1, 'EUR'); + */ + getReleaseStats: function ( + release: number, + currency?: Currency + ): Promise> { + let path = `/marketplace/stats/${release}`; + if (currency) { + path += `?${toQueryString({ curr_abbr: currency })}`; + } + // @ts-ignore + return client.get({ url: path }); + }, + }; +} diff --git a/lib/oauth.js b/lib/oauth.js deleted file mode 100644 index 35fb0e3..0000000 --- a/lib/oauth.js +++ /dev/null @@ -1,114 +0,0 @@ -'use strict'; - -var queryString = require('querystring'), - OAuth = require('oauth-1.0a'), - util = require('./util.js'), - Client = require('./client.js'); - -/** - * Default configuration - */ - -var defaultConfig = { - requestTokenUrl: 'https://api.discogs.com/oauth/request_token', - accessTokenUrl: 'https://api.discogs.com/oauth/access_token', - authorizeUrl: 'https://www.discogs.com/oauth/authorize', - version: '1.0', - signatureMethod: 'PLAINTEXT' // Or HMAC-SHA1 -}; - -module.exports = DiscogsOAuth; - -/** - * Object constructor - * @param {object} [auth] - Authentication object - * @returns {DiscogsOAuth} - */ - -function DiscogsOAuth(auth){ - this.config = util.merge({}, defaultConfig); - this.auth = {method: 'oauth', level: 0}; - if(auth && (typeof auth === 'object') && (auth.method === 'oauth')){ - util.merge(this.auth, auth); - } -} - -/** - * Override the default configuration - * @param {object} customConfig - Custom configuration object for Browserify/CORS/Proxy use cases - * @returns {DiscogsOAuth} - */ -DiscogsOAuth.prototype.setConfig = function(customConfig){ - util.merge(this.config, customConfig); - return this; -}; - -/** - * Get an OAuth request token from Discogs - * @param {string} consumerKey - The Discogs consumer key - * @param {string} consumerSecret - The Discogs consumer secret - * @param {string} callbackUrl - The url for redirection after obtaining the request token - * @param {function} [callback] - Callback function receiving the data - * @returns {DiscogsOAuth} - */ - -DiscogsOAuth.prototype.getRequestToken = function(consumerKey, consumerSecret, callbackUrl, callback){ - var auth = this.auth, config = this.config; - auth.consumerKey = consumerKey; - auth.consumerSecret = consumerSecret; - new Client(auth).get({url: config.requestTokenUrl+'?oauth_callback='+OAuth.prototype.percentEncode(callbackUrl), queue: false, json: false}, function(err, data){ - if(!err && data){ - data = queryString.parse(data); - auth.token = data.oauth_token; - auth.tokenSecret = data.oauth_token_secret; - auth.authorizeUrl = config.authorizeUrl+'?oauth_token='+data.oauth_token; - } - if(typeof callback === 'function'){ callback(err, auth); } - }); - return this; -}; - -/** - * Get an OAuth access token from Discogs - * @param {string} verifier - The OAuth 1.0a verification code returned by Discogs - * @param {function} [callback] - Callback function receiving the data - * @returns {DiscogsOAuth} - */ - -DiscogsOAuth.prototype.getAccessToken = function(verifier, callback){ - var auth = this.auth; - new Client(auth).get({url: this.config.accessTokenUrl+'?oauth_verifier='+OAuth.prototype.percentEncode(verifier), queue: false, json: false}, function(err, data){ - if(!err && data){ - data = queryString.parse(data); - auth.token = data.oauth_token; - auth.tokenSecret = data.oauth_token_secret; - auth.level = 2; - delete auth.authorizeUrl; - } - if(typeof callback === 'function'){ callback(err, auth); } - }); - return this; -}; - -/** - * Generic function to return the auth object - * @returns {object} - */ -DiscogsOAuth.prototype.export = function(){ - return this.auth; -}; - -/** - * Parse the OAuth HTTP header content - * @param {string} requestMethod - The upper case HTTP request method (GET, POST, etc) - * @param {string} url - The url that is to be accessed - * @returns {string} - */ -DiscogsOAuth.prototype.toHeader = function(requestMethod, url){ - var oAuth = new OAuth({ - consumer: {key: this.auth.consumerKey, secret: this.auth.consumerSecret}, - signature_method: this.config.signatureMethod, version: this.config.version - }), - authObj = oAuth.authorize({method: requestMethod, url: url}, {key: this.auth.token, secret: this.auth.tokenSecret}); - return oAuth.toHeader(authObj).Authorization; -}; \ No newline at end of file diff --git a/lib/oauth.ts b/lib/oauth.ts new file mode 100644 index 0000000..d5bf636 --- /dev/null +++ b/lib/oauth.ts @@ -0,0 +1,126 @@ +// import { parse } from 'querystring'; +// import OAuth, { prototype } from 'oauth-1.0a'; +// import { merge } from './util.js'; +// import Client from './client.js'; + +// /** +// * Default configuration +// */ +// let defaultConfig = { +// requestTokenUrl: 'https://api.discogs.com/oauth/request_token', +// accessTokenUrl: 'https://api.discogs.com/oauth/access_token', +// authorizeUrl: 'https://www.discogs.com/oauth/authorize', +// version: '1.0', +// signatureMethod: 'PLAINTEXT', // Or HMAC-SHA1 +// }; + +// /** +// * Object constructor +// * @param {object} [auth] - Authentication object +// * @returns {DiscogsOAuth} +// */ +// export default class DiscogsOAuth { +// constructor(auth) { +// this.config = merge({}, defaultConfig); +// this.auth = { method: 'oauth', level: 0 }; +// if (auth && typeof auth === 'object' && auth.method === 'oauth') { +// merge(this.auth, auth); +// } +// } +// /** +// * Override the default configuration +// * @param {object} customConfig - Custom configuration object for Browserify/CORS/Proxy use cases +// * @returns {DiscogsOAuth} +// */ +// setConfig(customConfig) { +// merge(this.config, customConfig); +// return this; +// } +// /** +// * Get an OAuth request token from Discogs +// * @param {string} consumerKey - The Discogs consumer key +// * @param {string} consumerSecret - The Discogs consumer secret +// * @param {string} callbackUrl - The url for redirection after obtaining the request token +// * @param {function} [callback] - Callback function receiving the data +// * @returns {DiscogsOAuth} +// */ +// getRequestToken(consumerKey, consumerSecret, callbackUrl, callback) { +// let auth = this.auth, +// config = this.config; +// auth.consumerKey = consumerKey; +// auth.consumerSecret = consumerSecret; +// new Client(auth).get( +// { +// url: config.requestTokenUrl + '?oauth_callback=' + prototype.percentEncode(callbackUrl), +// queue: false, +// json: false, +// }, +// function (err, data) { +// if (!err && data) { +// data = parse(data); +// auth.token = data.oauth_token; +// auth.tokenSecret = data.oauth_token_secret; +// auth.authorizeUrl = config.authorizeUrl + '?oauth_token=' + data.oauth_token; +// } +// if (typeof callback === 'function') { +// callback(err, auth); +// } +// } +// ); +// return this; +// } +// /** +// * Get an OAuth access token from Discogs +// * @param {string} verifier - The OAuth 1.0a verification code returned by Discogs +// * @param {function} [callback] - Callback function receiving the data +// * @returns {DiscogsOAuth} +// */ +// getAccessToken(verifier, callback) { +// let auth = this.auth; +// new Client(auth).get( +// { +// url: this.config.accessTokenUrl + '?oauth_verifier=' + prototype.percentEncode(verifier), +// queue: false, +// json: false, +// }, +// function (err, data) { +// if (!err && data) { +// data = parse(data); +// auth.token = data.oauth_token; +// auth.tokenSecret = data.oauth_token_secret; +// auth.level = 2; +// delete auth.authorizeUrl; +// } +// if (typeof callback === 'function') { +// callback(err, auth); +// } +// } +// ); +// return this; +// } +// /** +// * Generic function to return the auth object +// * @returns {object} +// */ +// export() { +// return this.auth; +// } +// /** +// * Parse the OAuth HTTP header content +// * @param {string} requestMethod - The upper case HTTP request method (GET, POST, etc) +// * @param {string} url - The url that is to be accessed +// * @returns {string} +// */ +// toHeader(requestMethod, url) { +// let oAuth = new OAuth({ +// consumer: { key: this.auth.consumerKey, secret: this.auth.consumerSecret }, +// signature_method: this.config.signatureMethod, +// version: this.config.version, +// }), +// authObj = oAuth.authorize( +// { method: requestMethod, url: url }, +// { key: this.auth.token, secret: this.auth.tokenSecret } +// ); +// return oAuth.toHeader(authObj).Authorization; +// } +// } diff --git a/lib/queue.js b/lib/queue.js deleted file mode 100644 index c226bd2..0000000 --- a/lib/queue.js +++ /dev/null @@ -1,130 +0,0 @@ -'use strict'; - -var DiscogsError = require('./error.js').DiscogsError, - util = require('./util.js'); - -module.exports = Queue; - -/** - * Default configuration - * @type {object} - */ - -var defaultConfig = { - maxStack: 20, // Max 20 calls queued in the stack - maxCalls: 60, // Max 60 calls per interval - interval: 60000, // 1 minute interval -}; - -/** - * Object constructor - * @param {object} [customConfig] - Optional custom configuration object - * @returns {Queue} - */ - -function Queue(customConfig){ - // Allow the class to be called as a function, returning an instance - if(!(this instanceof Queue)){ - return new Queue(customConfig); - } - // Set the default configuration - this.config = util.merge({}, defaultConfig); - if(customConfig && (typeof customConfig === 'object')){ - this.setConfig(customConfig); - } - this._stack = []; - this._firstCall = 0; - this._callCount = 0; -} - -/** - * Override the default configuration - * @param {object} customConfig - Custom configuration object - * @returns {object} - */ - -Queue.prototype.setConfig = function(customConfig){ - util.merge(this.config, customConfig); - return this; -}; - -/** - * Add a function to the queue. Usage: - * - * queue.add(function(err, freeCallsRemaining, freeStackPositionsRemaining){ - * if(!err){ - * // Do something - * } - * }); - * - * @param {function} callback - The function to schedule for execution - * @returns {object} - */ - -Queue.prototype.add = function(callback){ - if(this._stack.length === 0){ - var now = Date.now(); - // Within call interval limits: Just execute the callback - if(this._callCount < this.config.maxCalls){ - this._callCount++; - if(this._callCount === 1){ - this._firstCall = now; - } - setTimeout(callback, 0, null, (this.config.maxCalls - this._callCount), this.config.maxStack); - // Upon reaching the next interval: Execute callback and reset - }else if((now - this._firstCall) > this.config.interval){ - this._callCount = 1; - this._firstCall = now; - setTimeout(callback, 0, null, (this.config.maxCalls - this._callCount), this.config.maxStack); - // Within the interval exceeding call limit: Queue the call - }else{ - this._pushStack(callback); - } - // Current stack is not empty and must be processed first, queue new calls - }else{ - this._pushStack(callback); - } - return this; -}; - -/** - * Push a callback on the callback stack to be executed - * @param {function} callback - */ - -Queue.prototype._pushStack = function(callback){ - if(this._stack.length < this.config.maxStack){ - var factor = Math.ceil((this._stack.length / this.config.maxCalls)), - timeout = ((this._firstCall + (this.config.interval * factor)) - Date.now()) + (this._stack.length % this.config.maxCalls) + 1; - this._stack.push({ - callback: callback, - timeout: setTimeout(this._callStack, timeout, this) - }); - }else{ // Queue max length exceeded: Pass an error to the callback - setTimeout(callback, 0, new DiscogsError(429, 'Too many requests'), 0, 0); - } -}; - -/** - * Shift a function from the callback stack and call it - * @param {Queue} [queue] - Async calls need the queue instance - */ - -Queue.prototype._callStack = function(queue){ - queue = queue||this; - queue._stack.shift().callback.call(queue, null, 0, (queue.config.maxStack - queue._stack.length)); - queue._callCount++; -}; - -/** - * Clear the request stack. All queued requests/callbacks will be cancelled! - * @returns {object} - */ - -Queue.prototype.clear = function(){ - var item; - while(item = this._stack.shift()){ - clearTimeout(item.timeout); - } - return this; -}; \ No newline at end of file diff --git a/lib/queue.ts b/lib/queue.ts new file mode 100644 index 0000000..832808d --- /dev/null +++ b/lib/queue.ts @@ -0,0 +1,133 @@ +import { DiscogsError } from './error.js'; +import { merge } from './util.js'; + +type QueueConfig = { + maxStack: number; + maxCalls: number; + interval: number; +}; + +/** + * Default configuration + */ +let defaultConfig: QueueConfig = { + maxStack: 20, // Max 20 calls queued in the stack + maxCalls: 60, // Max 60 calls per interval + interval: 60000, // 1 minute interval +}; + +export default class Queue { + config: QueueConfig; + stack: Array<{ callback: Function; timeout: NodeJS.Timeout }>; + firstCall: number; + callCount: number; + + /** + * Object constructor + * @param {QueueConfig} [customConfig] - Optional custom configuration object + */ + constructor(customConfig?: Partial) { + // Set the default configuration + // @ts-ignore + this.config = merge({}, defaultConfig); + if (customConfig && typeof customConfig === 'object') { + this.setConfig(customConfig); + } + this.stack = []; + this.firstCall = 0; + this.callCount = 0; + } + + /** + * Override the default configuration + * @param {Partial} customConfig - Custom configuration object + * @returns {object} + */ + setConfig(customConfig: Partial): object { + merge(this.config, customConfig); + return this; + } + + /** + * Add a function to the queue. Usage: + * + * @example + * queue.add(function(err, freeCallsRemaining, freeStackPositionsRemaining){ + * if(!err){ + * // Do something + * } + * }); + * + * @param {(err: Error | null, freeCallsRemaining: number, freeStackPositionsRemaining: number) => any} callback - The function to schedule for execution + * @returns {object} + */ + add(callback: (err: Error | null, freeCallsRemaining: number, freeStackPositionsRemaining: number) => any): object { + if (this.stack.length === 0) { + let now = Date.now(); + // Within call interval limits: Just execute the callback + if (this.callCount < this.config.maxCalls) { + this.callCount++; + if (this.callCount === 1) { + this.firstCall = now; + } + setTimeout(callback, 0, null, this.config.maxCalls - this.callCount, this.config.maxStack); + // Upon reaching the next interval: Execute callback and reset + } else if (now - this.firstCall > this.config.interval) { + this.callCount = 1; + this.firstCall = now; + setTimeout(callback, 0, null, this.config.maxCalls - this.callCount, this.config.maxStack); + // Within the interval exceeding call limit: Queue the call + } else { + this._pushStack(callback); + } + // Current stack is not empty and must be processed first, queue new calls + } else { + this._pushStack(callback); + } + return this; + } + + /** + * Push a callback on the callback stack to be executed + * @param {Function} callback + */ + _pushStack(callback: Function) { + if (this.stack.length < this.config.maxStack) { + let factor = Math.ceil(this.stack.length / this.config.maxCalls), + timeout = + this.firstCall + + this.config.interval * factor - + Date.now() + + (this.stack.length % this.config.maxCalls) + + 1; + this.stack.push({ + callback: callback, + timeout: setTimeout(this._callStack, timeout, this), + }); + } else { + // Queue max length exceeded: Pass an error to the callback + setTimeout(callback, 0, new DiscogsError(429, 'Too many requests'), 0, 0); + } + } + + /** + * Shift a function from the callback stack and call it + * @param {Queue} [queue] - Async calls need the queue instance + */ + _callStack(queue?: Queue) { + queue = queue || this; + queue.stack.shift()?.callback.call(queue, null, 0, queue.config.maxStack - queue.stack.length); + queue.callCount++; + } + + /** + * Clear the request stack. All queued requests/callbacks will be cancelled! + */ + clear() { + let item; + while ((item = this.stack.shift())) { + clearTimeout(item.timeout); + } + return this; + } +} diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..856a147 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,186 @@ +export type RateLimitedResponse = { data: ResponseData; rateLimit: RateLimit }; +export type ClientConfig = { + host: string; + port: number; + userAgent: string; + apiVersion: string; + outputFormat: 'discogs' | 'plaintext' | 'html'; + requestLimit: number; + requestLimitAuth: number; + requestLimitInterval: number; +}; +export type Auth = { + method: 'discogs' | 'oauth'; + level: number; + consumerKey: string; + consumerSecret: string; + userToken: string; +}; +export type RateLimit = { limit: number; used: number; remaining: number }; +export type RequestCallback = (err?: Error, data?: unknown, rateLimit?: RateLimit) => any; +export type RequestOptions = { + url: string; + method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + data?: Record; + queue?: boolean; + json?: boolean; + authLevel?: number; +}; +export type PaginationResponse = { + pagination: { + per_page: number; + pages: number; + page: number; + items: number; + urls: { next: string; last: string; first?: string; prev?: string }; + }; +}; +export type GetIdentityResponse = { id: number; username: string; resource_url: string; consumer_name: string }; +/** + * Some resources represent collections of objects and may be paginated. By default, 50 items per page are shown. + * To browse different pages, or change the number of items per page (up to 100), use the page and per_page parameters + */ +export type PaginationParameters = Partial<{ page: number; per_page: number }>; +export type SortOrder = 'asc' | 'desc'; +export type Seller = { username: string; resource_url: string; id: number }; +export type Release = { + catalog_number: string; + resource_url: string; + year: number; + id: number; + description: string; + images: Array; + artist: string; + title: string; + format: string; + thumbnail: string; + stats: { + community: { in_wantlist: number; in_collection: number }; + user?: { in_wantlist: number; in_collection: number }; + }; +}; +export type Listing = { + weight?: string; + format_quantity?: number; + external_id?: number; + location?: string; + in_cart?: boolean; + status: string; + price: Price; + original_price: OriginalPrice; + shipping_price: Price; + original_shipping_price: Price; + allow_offers: boolean; + sleeve_condition: string; + id: number; + condition: string; + posted: string; + ships_from: string; + uri: string; + comments: string; + seller: Seller; + release: Release; + resource_url: string; + audio: boolean; +}; +export type Price = { currency: Currency; value: number }; +export type OriginalPrice = { curr_abbr: Currency; curr_id: number; formatted: string; value: number }; +export type Label = { + id: number; + name: string; + resource_url: string; + uri: string; + releases_url: string; + images?: Array; + contactinfo?: string; + profile: string; + data_quality: string; + sublabels?: Array; + parentLabel?: string; + urls?: Array; +}; +export type LabelShort = { resource_url: string; entity_type: string; catno: string; id: number; name: string }; +export type Currency = 'USD' | 'GBP' | 'EUR' | 'CAD' | 'AUD' | 'JPY' | 'CHF' | 'MXN' | 'BRL' | 'NZD' | 'SEK' | 'ZAR'; +export type SortParameters = Partial<{ sort: K; sort_order: SortOrder }>; +export type GetReleaseResponse = { + title: string; + id: number; + artists: Array; + data_quality: string; + thumb: string; + community: Array<{ + contributors: Array<{ resource_url: string; username: string }>; + data_quality: string; + have: number; + rating: { average: number; count: number }; + status: string; + submitter: { resource_url: string; username: string }; + want: number; + }>; + companies: Array<{ + catno: string; + entity_type: string; + entity_type_name: string; + id: number; + name: string; + resource_url: string; + }>; + country: string; + date_added: string; + date_changed: string; + estimated_weight: number; + extraartists: Array; + format_quantity: number; + formats: Array<{ descriptions: Array; name: string; qty: string }>; + genres: Array; + identifiers: Array<{ type: string; value: string }>; + images: Array; + labels: Array<{ catno: string; entity_type: string; id: number; name: string; resource_url: string }>; + lowest_price: number; + master_id: number; + master_url: string; + notes: string; + num_for_sale: number; + released: string; + released_formatted: string; + resource_url: string; + series: Array<{ + name: string; + catno: string; + entity_type: string; + entity_type_name: string; + id: number; + resource_url: string; + thumbnail_url: string; + }>; + status: string; + styles: Array; + tracklist: Array; + uri: string; + videos: Array<{ description: string; duration: number; embed: boolean; title: string; uri: string }>; + year: number; +}; +export type Image = { + width: number; + height: number; + resource_url: string; + type: string; + uri: string; + uri150: string; +}; +export type Artist = { + anv: string; + id: number; + join: string; + name: string; + resource_url: string; + role: string; + tracks: string; +}; +export type Tracklisting = { + duration: string; + position: string; + title: string; + type_: string; + extraartists?: Array; +}; diff --git a/lib/user.js b/lib/user.js deleted file mode 100644 index fa38e7c..0000000 --- a/lib/user.js +++ /dev/null @@ -1,125 +0,0 @@ -'use strict'; - -var util = require('./util.js'); - -module.exports = function(client){ - var user = {}; - - /** - * Get the profile for the given user - * @param {string} user - The user name - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - user.getProfile = function(user, callback){ - return client.get('/users/'+util.escape(user), callback); - }; - - /** - * Get the inventory for the given user - * @param {string} user - The user name - * @param {object} [params] - Extra params like status, sort and sort_order, pagination - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - user.getInventory = function(user, params, callback){ - var path = '/users/'+util.escape(user)+'/inventory'; - if((arguments.length === 2) && (typeof params === 'function')){ - callback = params; - }else{ // Add pagination params when present - path = util.addParams(path, params); - } - return client.get(path, callback); - }; - - /** - * Copy the client getIdentity function to the user module - */ - - user.getIdentity = client.getIdentity; - - /** - * Expose the collection functions and pass the client instance - * @returns {object} - */ - - user.collection = function(){ - return require('./collection.js')(client); - }; - - /** - * Expose the wantlist functions and pass the client instance - * @returns {object} - */ - - user.wantlist = function(){ - return require('./wantlist.js')(client); - }; - - /** - * Expose the list functions and pass the client instance - * @returns {object} - */ - - user.list = function(){ - return require('./list.js')(client); - }; - - /** - * Get the contributions for the given user - * @param {string} user - The user name - * @param {object} [params] - Optional pagination and sorting params - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - user.getContributions = function(user, params, callback){ - var path = '/users/'+util.escape(user)+'/contributions'; - if((arguments.length === 2) && (typeof params === 'function')){ - callback = params; - }else{ // Add pagination params when present - path = util.addParams(path, params); - } - return client.get(path, callback); - }; - - /** - * Get the submissions for the given user - * @param {string} user - The user name - * @param {object} [params] - Optional pagination params - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - user.getSubmissions = function(user, params, callback){ - var path = '/users/'+util.escape(user)+'/submissions'; - if((arguments.length === 2) && (typeof params === 'function')){ - callback = params; - }else{ // Add pagination params when present - path = util.addParams(path, params); - } - return client.get(path, callback); - }; - - /** - * Get the lists for the given user - * @param {string} user - The user name - * @param {object} [params] - Optional pagination params - * @param {function} [callback] - The callback - * @return {DiscogsClient|Promise} - */ - - user.getLists = function(user, params, callback){ - var path = '/users/'+util.escape(user)+'/lists'; - if((arguments.length === 2) && (typeof params === 'function')){ - callback = params; - }else{ // Add pagination params when present - path = util.addParams(path, params); - } - return client.get(path, callback); - }; - - return user; -}; \ No newline at end of file diff --git a/lib/user.ts b/lib/user.ts new file mode 100644 index 0000000..2946f64 --- /dev/null +++ b/lib/user.ts @@ -0,0 +1,249 @@ +import { type DiscogsClient } from './client.js'; +import { + type RateLimitedResponse, + type PaginationParameters, + type PaginationResponse, + type Currency, + type Listing, + type SortParameters, + type GetReleaseResponse, + type Label, +} from './types.js'; +import collection from './collection.js'; +import list from './list.js'; +import { escape, toQueryString } from './util.js'; +import wantlist from './wantlist.js'; + +type GetProfileResponse = { + email?: string; + num_unread?: number; + activated: boolean; + marketplace_suspended: boolean; + is_staff: boolean; + profile: string; + wantlist_url: string; + rank: number; + num_pending: number; + id: number; + num_for_sale: number; + home_page: string; + location: string; + collection_folders_url: string; + username: string; + collection_fields_url: string; + releases_contributed: number; + registered: string; + rating_avg: number; + num_collection: number; + releases_rated: number; + num_lists: number; + name: string; + num_wantlist: number; + inventory_url: string; + avatar_url: string; + banner_url: string; + uri: string; + resource_url: string; + buyer_rating: number; + buyer_rating_stars: number; + buyer_num_ratings: number; + seller_rating: number; + seller_rating_stars: number; + seller_num_ratings: number; + curr_abbr: Currency; +}; +type GetInventoryResponse = { listings: Array }; +type GetContributionsResponse = { contributions: Array }; +type GetSubmissionsResponse = { + submissions: { + artists: Array<{ + data_quality: string; + id: number; + name: string; + namevariations?: Array; + releases_url: string; + resource_url: string; + uri: string; + }>; + labels: Array