gerdur 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.4.0 - 2026-08-31
4
+
5
+ ### Added
6
+
7
+ - **`gerdur-core@^2.3.0`** — re-exports `getTrackPreview`, `downloadPreview`,
8
+ `formatName`, `toFormat`, `DEEZER_FORMATS` and the `DeezerFormat` /
9
+ `TrackPreview` types.
10
+ - **`--preview`** — download the 30-second clip for each track as
11
+ `<name>.preview.mp3` instead of the full file. Plain licence-free MP3s: no
12
+ `--quality`, no decryption, no tagging. Works with every source and headless.
13
+ - **`Session.trackPreview(track)`** and **`Session.downloadPreview(track)`**.
14
+
3
15
  ## 2.3.0 - 2026-08-31
4
16
 
5
17
  ### Added
package/README.md CHANGED
@@ -142,6 +142,7 @@ All options are optional. You can suppress prompts via providing `quality` and `
142
142
  | `--bpm-min` / `--bpm-max` | *None* | number | Search filter — tempo range (beats per minute) |
143
143
  | `--dur-min` / `--dur-max` | *None* | number (seconds) | Search filter — track duration range |
144
144
  | `--search-limit` | *None* | number (default 50) | How many search results to fetch |
145
+ | `--preview` | *None* | *Nothing* | Download the 30-second preview clips (`.preview.mp3`) instead of full tracks — no `--quality` needed |
145
146
 
146
147
  ## Search
147
148
 
@@ -198,6 +199,18 @@ only on **track** search:
198
199
  | `dur_min:` / `dur_max:` | `dur_min:200` (seconds) |
199
200
  | `bpm_min:` / `bpm_max:` | `bpm_min:120` |
200
201
 
202
+ ## Previews
203
+
204
+ `--preview` writes the 30-second clip for each track as `<name>.preview.mp3`
205
+ instead of downloading the full file. The clips are plain, licence-free MP3s —
206
+ no `--quality`, no decryption, no tagging. Works with every source (URL, search,
207
+ `isrc:` / `upc:`) and in `--headless` mode.
208
+
209
+ ```bash
210
+ gerdur --preview -u https://deezer.com/album/302127 # 14 clips
211
+ gerdur -d --preview --artist "Justice" --search-limit 10 # audition a search
212
+ ```
213
+
201
214
  ## Programmatic API
202
215
 
203
216
  When installed as a dependency, `gerdur` exposes a side-effect-free API
@@ -246,7 +259,7 @@ Session methods:
246
259
  `artistTopTracks`, `relatedArtists`, `artistAlbums`, `artistRadio`,
247
260
  `trackByISRC`, `albumByUPC`
248
261
  - **user / download** — `getUser`, `getTrackBuffer`, `downloadTrack`,
249
- `downloadTracks`, `downloadUrl`
262
+ `downloadTracks`, `downloadUrl`, `trackPreview`, `downloadPreview`
250
263
 
251
264
  Every download call is silent; `downloadTracks` / `downloadUrl` accept
252
265
  `concurrency` and an `onProgress` callback and return one `{path, written} | null`
@@ -302,7 +315,8 @@ or `createSession`):
302
315
  `getEditorialCharts`, `getArtistTopTracks`, `getRelatedArtists`,
303
316
  `getArtistAlbums`, `getArtistPlaylists`, `getArtistRadioTracks`,
304
317
  `getTrackByISRC`, `getAlbumByUPC`
305
- - **Download** — `getTrackDownloadUrl`, `resolveDownloadUrls`, `GeoBlocked`
318
+ - **Download** — `getTrackDownloadUrl`, `resolveDownloadUrls`, `getTrackPreview`,
319
+ `downloadPreview`, `formatName`, `toFormat`, `DEEZER_FORMATS`, `GeoBlocked`
306
320
 
307
321
  ### Auth & config helpers
308
322
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "The crucial streaming module for dabox systems.",
5
5
  "author": "Christian",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -48,7 +48,7 @@
48
48
  "adm-zip": "^0.5.16",
49
49
  "chalk": "^4.1.2",
50
50
  "commander": "^9.5.0",
51
- "gerdur-core": "^2.2.0",
51
+ "gerdur-core": "^2.3.0",
52
52
  "dot-prop": "^6.0.1",
53
53
  "got": "^11.8.6",
54
54
  "gradient-string": "^2.0.2",
@@ -57,7 +57,8 @@ const cmd = new commander_1.Command()
57
57
  .option('--bpm-max <n>', 'Search filter: maximum BPM')
58
58
  .option('--dur-min <seconds>', 'Search filter: minimum duration')
59
59
  .option('--dur-max <seconds>', 'Search filter: maximum duration')
60
- .option('--search-limit <n>', 'Max search results to fetch', '50');
60
+ .option('--search-limit <n>', 'Max search results to fetch', '50')
61
+ .option('--preview', 'Download the 30-second preview clips instead of full tracks', false);
61
62
  if (process.pkg) {
62
63
  cmd.option('-U, --update', 'Update this program to latest version');
63
64
  }
@@ -79,7 +80,7 @@ const toFiniteNumber = (value) => {
79
80
  };
80
81
  const advancedFilters = (0, search_1.advancedFiltersFromFlags)(options);
81
82
  let advancedFiltersConsumed = false;
82
- if (options.headless && !options.quality) {
83
+ if (options.headless && !options.quality && !options.preview) {
83
84
  console.error(signale_1.default.error('Missing parameters --quality'));
84
85
  console.error(signale_1.default.note('Quality must be provided with headless mode'));
85
86
  process.exit(1);
@@ -152,10 +153,48 @@ const resolveAdvancedSearch = async (filters) => {
152
153
  }
153
154
  return { info: { type: 'track', id: query }, linktype: 'track', linkinfo: {}, tracks };
154
155
  };
156
+ /**
157
+ * `--preview`: fetch the 30-second clip for each selected track and write it as
158
+ * `<saveLayout path>.preview.mp3`. Bypasses the get_url / decrypt / tag pipeline
159
+ * entirely — previews are plain, licence-free MP3s.
160
+ */
161
+ const downloadPreviews = async (tracks, info, template, totalTracks, trackNumber, overwrite) => {
162
+ const saved = [];
163
+ await queue.addAll(tracks.map((track, index) => async () => {
164
+ const rel = (0, util_1.saveLayout)({
165
+ track,
166
+ album: info,
167
+ path: template,
168
+ trackNumber,
169
+ minimumIntegerDigits: totalTracks >= 100 ? 3 : 2,
170
+ }) + '.preview.mp3';
171
+ (0, log_update_1.default)(signale_1.default.pending(`(${index + 1}/${tracks.length}) ${track.ART_NAME} - ${track.SNG_TITLE}`));
172
+ if (!overwrite && (0, fs_1.existsSync)(rel)) {
173
+ saved.push(rel);
174
+ return;
175
+ }
176
+ try {
177
+ const clip = await (0, gerdur_core_1.downloadPreview)(track);
178
+ if (!clip) {
179
+ (0, log_update_1.default)(signale_1.default.warn(`No preview for ${track.SNG_TITLE}`));
180
+ return;
181
+ }
182
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(rel), { recursive: true });
183
+ (0, fs_1.writeFileSync)(rel, clip);
184
+ saved.push(rel);
185
+ (0, log_update_1.default)(signale_1.default.success(`${track.ART_NAME} - ${track.SNG_TITLE} (preview)`));
186
+ }
187
+ catch (err) {
188
+ (0, log_update_1.default)(signale_1.default.error(`${track.SNG_TITLE}: ${err.message}`));
189
+ }
190
+ }));
191
+ log_update_1.default.done();
192
+ return saved;
193
+ };
155
194
  const startDownload = async (saveLayout, url, skipPrompt) => {
156
195
  try {
157
196
  url = url ?? '';
158
- if (!options.quality) {
197
+ if (!options.quality && !options.preview) {
159
198
  const { musicQuality } = await (0, prompts_1.default)([
160
199
  {
161
200
  type: 'select',
@@ -327,56 +366,63 @@ const startDownload = async (saveLayout, url, skipPrompt) => {
327
366
  const resolveFullPath = options.resolveFullPath ?? conf.get('playlist.resolveFullPath');
328
367
  const savedFiles = [];
329
368
  let m3u8 = [];
330
- // One batched get_url for the whole selection instead of a request per track.
331
- // Deezer returns the best licensed format, so pass an ordered preference list.
332
- const QUALITY_PREF = {
333
- '1': [1],
334
- '128': [1],
335
- '3': [3, 1],
336
- '320': [3, 1],
337
- '9': [9, 3, 1],
338
- flac: [9, 3, 1],
339
- };
340
- const prefetchedUrls = new Map();
341
- if (data.tracks.length > 1 && !process.env.SIMULATE) {
342
- try {
343
- const base = QUALITY_PREF[String(options.quality).toLowerCase()] ?? [3, 1];
344
- const pref = fallbackQuality ? base : [base[0]];
345
- const resolved = await (0, gerdur_core_1.resolveDownloadUrls)(data.tracks, pref);
346
- resolved.forEach((r, i) => {
347
- if (r) {
348
- prefetchedUrls.set(data.tracks[i].SNG_ID, r);
349
- }
350
- });
351
- }
352
- catch {
353
- // batch resolve failed wholesale — every track falls back to per-track resolution
354
- }
369
+ const layoutTemplate = options.output ? options.output : saveLayout[data.linktype];
370
+ if (options.preview) {
371
+ const clips = await downloadPreviews(data.tracks, data.linkinfo, layoutTemplate, data.tracks.length, trackNumber, overwrite);
372
+ savedFiles.push(...clips);
355
373
  }
356
- await queue.addAll(data.tracks.map((track, index) => {
357
- return async () => {
358
- const savedPath = await (0, download_track_1.default)({
359
- track,
360
- quality: options.quality,
361
- info: data.linkinfo,
362
- coverSizes,
363
- path: options.output ? options.output : saveLayout[data.linktype],
364
- totalTracks: data ? data.tracks.length : 10,
365
- trackNumber,
366
- fallbackTrack,
367
- fallbackQuality,
368
- overwrite,
369
- message: `(${index}/${data.tracks.length})`,
370
- lrc,
371
- prefetched: prefetchedUrls.get(track.SNG_ID) ?? null,
372
- });
373
- // Add to saved list
374
- if (savedPath) {
375
- m3u8.push((0, path_1.resolve)(process.env.SIMULATE ? savedPath : (0, true_case_path_1.trueCasePathSync)(savedPath)));
376
- savedFiles.push(savedPath);
377
- }
374
+ else {
375
+ // One batched get_url for the whole selection instead of a request per track.
376
+ // Deezer returns the best licensed format, so pass an ordered preference list.
377
+ const QUALITY_PREF = {
378
+ '1': [1],
379
+ '128': [1],
380
+ '3': [3, 1],
381
+ '320': [3, 1],
382
+ '9': [9, 3, 1],
383
+ flac: [9, 3, 1],
378
384
  };
379
- }));
385
+ const prefetchedUrls = new Map();
386
+ if (data.tracks.length > 1 && !process.env.SIMULATE) {
387
+ try {
388
+ const base = QUALITY_PREF[String(options.quality).toLowerCase()] ?? [3, 1];
389
+ const pref = fallbackQuality ? base : [base[0]];
390
+ const resolved = await (0, gerdur_core_1.resolveDownloadUrls)(data.tracks, pref);
391
+ resolved.forEach((r, i) => {
392
+ if (r) {
393
+ prefetchedUrls.set(data.tracks[i].SNG_ID, r);
394
+ }
395
+ });
396
+ }
397
+ catch {
398
+ // batch resolve failed wholesale — every track falls back to per-track resolution
399
+ }
400
+ }
401
+ await queue.addAll(data.tracks.map((track, index) => {
402
+ return async () => {
403
+ const savedPath = await (0, download_track_1.default)({
404
+ track,
405
+ quality: options.quality,
406
+ info: data.linkinfo,
407
+ coverSizes,
408
+ path: layoutTemplate,
409
+ totalTracks: data ? data.tracks.length : 10,
410
+ trackNumber,
411
+ fallbackTrack,
412
+ fallbackQuality,
413
+ overwrite,
414
+ message: `(${index}/${data.tracks.length})`,
415
+ lrc,
416
+ prefetched: prefetchedUrls.get(track.SNG_ID) ?? null,
417
+ });
418
+ // Add to saved list
419
+ if (savedPath) {
420
+ m3u8.push((0, path_1.resolve)(process.env.SIMULATE ? savedPath : (0, true_case_path_1.trueCasePathSync)(savedPath)));
421
+ savedFiles.push(savedPath);
422
+ }
423
+ };
424
+ }));
425
+ }
380
426
  // Display downloaded location
381
427
  if (savedFiles.length > 0) {
382
428
  const savedIn = new Set(savedFiles.map((l) => (0, path_1.dirname)(l)));
@@ -59,6 +59,7 @@ export type { SessionOptions, DownloadTracksOptions, SearchType } from './lib/se
59
59
  export { getTrackBuffer, getTaggedTrack, downloadTrackToFile } from './lib/api-download';
60
60
  export type { Quality, GetTrackBufferOptions, DownloadTrackOptions, DownloadResult } from './lib/api-download';
61
61
  export { default as Config, globalConfigPath, resolveConfigFile } from './lib/config';
62
- export { initDeezerApi, parseInfo, searchMusic, searchPublicApi, searchTracks, searchAlbums, searchArtists, searchPlaylists, buildAdvancedQuery, suggest, getGenres, getChart, getChartTracks, getGenreArtists, getEditorialList, getEditorialReleases, getEditorialSelection, getEditorialCharts, getArtistTopTracks, getRelatedArtists, getArtistAlbums, getArtistPlaylists, getArtistRadioTracks, getTrackByISRC, getAlbumByUPC, getUser, getTrackInfo, getAlbumInfo, getAlbumTracks, getPlaylistInfo, getPlaylistTracks, getArtistInfo, getDiscography, getLyrics, getTrackDownloadUrl, resolveDownloadUrls, addTrackTags, getRichAlbum, normalizeContributors, toLrc, GeoBlocked, } from 'gerdur-core';
62
+ export { initDeezerApi, parseInfo, searchMusic, searchPublicApi, searchTracks, searchAlbums, searchArtists, searchPlaylists, buildAdvancedQuery, suggest, getGenres, getChart, getChartTracks, getGenreArtists, getEditorialList, getEditorialReleases, getEditorialSelection, getEditorialCharts, getArtistTopTracks, getRelatedArtists, getArtistAlbums, getArtistPlaylists, getArtistRadioTracks, getTrackByISRC, getAlbumByUPC, getTrackPreview, downloadPreview, formatName, toFormat, DEEZER_FORMATS, getUser, getTrackInfo, getAlbumInfo, getAlbumTracks, getPlaylistInfo, getPlaylistTracks, getArtistInfo, getDiscography, getLyrics, getTrackDownloadUrl, resolveDownloadUrls, addTrackTags, getRichAlbum, normalizeContributors, toLrc, GeoBlocked, } from 'gerdur-core';
63
63
  export type { AddTrackTagsOptions, TaggedTrack, TrackTagModel, RichAlbum, ResolvedUrl, NormalizedContributors, } from 'gerdur-core';
64
64
  export type { advancedSearchFilters, searchOrder, searchEntity, publicApiSearchOptions, publicApiSearchResponse, searchResultTrack, searchResultAlbum, searchResultArtist, searchResultPlaylist, suggestResult, publicApiList, chartType, chartTrack, chartAlbum, chartArtist, chartPlaylist, chartPodcast, genreType, editorialType, artistAlbumResult, } from 'gerdur-core/types';
65
+ export type { Quality as MediaFormat, DeezerFormat, TrackPreview } from 'gerdur-core';
package/dist/src/index.js CHANGED
@@ -33,8 +33,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
33
33
  return (mod && mod.__esModule) ? mod : { "default": mod };
34
34
  };
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.normalizeContributors = exports.getRichAlbum = exports.addTrackTags = exports.resolveDownloadUrls = exports.getTrackDownloadUrl = exports.getLyrics = exports.getDiscography = exports.getArtistInfo = exports.getPlaylistTracks = exports.getPlaylistInfo = exports.getAlbumTracks = exports.getAlbumInfo = exports.getTrackInfo = exports.getUser = exports.getAlbumByUPC = exports.getTrackByISRC = exports.getArtistRadioTracks = exports.getArtistPlaylists = exports.getArtistAlbums = exports.getRelatedArtists = exports.getArtistTopTracks = exports.getEditorialCharts = exports.getEditorialSelection = exports.getEditorialReleases = exports.getEditorialList = exports.getGenreArtists = exports.getChartTracks = exports.getChart = exports.getGenres = exports.suggest = exports.buildAdvancedQuery = exports.searchPlaylists = exports.searchArtists = exports.searchAlbums = exports.searchTracks = exports.searchPublicApi = exports.searchMusic = exports.parseInfo = exports.initDeezerApi = exports.resolveConfigFile = exports.globalConfigPath = exports.Config = exports.downloadTrackToFile = exports.getTaggedTrack = exports.getTrackBuffer = exports.Session = exports.createSession = exports.getArl = exports.LoginError = exports.loginWithEmail = void 0;
37
- exports.GeoBlocked = exports.toLrc = void 0;
36
+ exports.getLyrics = exports.getDiscography = exports.getArtistInfo = exports.getPlaylistTracks = exports.getPlaylistInfo = exports.getAlbumTracks = exports.getAlbumInfo = exports.getTrackInfo = exports.getUser = exports.DEEZER_FORMATS = exports.toFormat = exports.formatName = exports.downloadPreview = exports.getTrackPreview = exports.getAlbumByUPC = exports.getTrackByISRC = exports.getArtistRadioTracks = exports.getArtistPlaylists = exports.getArtistAlbums = exports.getRelatedArtists = exports.getArtistTopTracks = exports.getEditorialCharts = exports.getEditorialSelection = exports.getEditorialReleases = exports.getEditorialList = exports.getGenreArtists = exports.getChartTracks = exports.getChart = exports.getGenres = exports.suggest = exports.buildAdvancedQuery = exports.searchPlaylists = exports.searchArtists = exports.searchAlbums = exports.searchTracks = exports.searchPublicApi = exports.searchMusic = exports.parseInfo = exports.initDeezerApi = exports.resolveConfigFile = exports.globalConfigPath = exports.Config = exports.downloadTrackToFile = exports.getTaggedTrack = exports.getTrackBuffer = exports.Session = exports.createSession = exports.getArl = exports.LoginError = exports.loginWithEmail = void 0;
37
+ exports.GeoBlocked = exports.toLrc = exports.normalizeContributors = exports.getRichAlbum = exports.addTrackTags = exports.resolveDownloadUrls = exports.getTrackDownloadUrl = void 0;
38
38
  // ─── Authentication ────────────────────────────────────────────────────────
39
39
  const email_login_1 = require("./lib/email-login");
40
40
  Object.defineProperty(exports, "loginWithEmail", { enumerable: true, get: function () { return email_login_1.loginWithEmail; } });
@@ -112,6 +112,11 @@ Object.defineProperty(exports, "getArtistPlaylists", { enumerable: true, get: fu
112
112
  Object.defineProperty(exports, "getArtistRadioTracks", { enumerable: true, get: function () { return gerdur_core_1.getArtistRadioTracks; } });
113
113
  Object.defineProperty(exports, "getTrackByISRC", { enumerable: true, get: function () { return gerdur_core_1.getTrackByISRC; } });
114
114
  Object.defineProperty(exports, "getAlbumByUPC", { enumerable: true, get: function () { return gerdur_core_1.getAlbumByUPC; } });
115
+ Object.defineProperty(exports, "getTrackPreview", { enumerable: true, get: function () { return gerdur_core_1.getTrackPreview; } });
116
+ Object.defineProperty(exports, "downloadPreview", { enumerable: true, get: function () { return gerdur_core_1.downloadPreview; } });
117
+ Object.defineProperty(exports, "formatName", { enumerable: true, get: function () { return gerdur_core_1.formatName; } });
118
+ Object.defineProperty(exports, "toFormat", { enumerable: true, get: function () { return gerdur_core_1.toFormat; } });
119
+ Object.defineProperty(exports, "DEEZER_FORMATS", { enumerable: true, get: function () { return gerdur_core_1.DEEZER_FORMATS; } });
115
120
  Object.defineProperty(exports, "getUser", { enumerable: true, get: function () { return gerdur_core_1.getUser; } });
116
121
  Object.defineProperty(exports, "getTrackInfo", { enumerable: true, get: function () { return gerdur_core_1.getTrackInfo; } });
117
122
  Object.defineProperty(exports, "getAlbumInfo", { enumerable: true, get: function () { return gerdur_core_1.getAlbumInfo; } });
@@ -3,6 +3,7 @@ import { parseInfo, searchMusic } from 'gerdur-core';
3
3
  import type { AdvancedSearchOptions } from './search';
4
4
  import type { Quality, DownloadTrackOptions, DownloadResult } from './api-download';
5
5
  import type { trackType, userType, advancedSearchFilters, publicApiSearchResponse, searchResultTrack, suggestResult, chartType, genreType, editorialType, artistAlbumResult, searchResultArtist, publicApiList, trackTypePublicApi, albumTypePublicApi } from 'gerdur-core/types';
6
+ import type { TrackPreview } from 'gerdur-core';
6
7
  /** Search result categories accepted by {@link Session.search}. */
7
8
  export type SearchType = 'ALBUM' | 'ARTIST' | 'TRACK' | 'PLAYLIST' | 'RADIO' | 'SHOW' | 'USER' | 'LIVESTREAM' | 'CHANNEL';
8
9
  export interface SessionOptions {
@@ -85,6 +86,10 @@ export declare class Session {
85
86
  trackByISRC(isrc: string): Promise<trackTypePublicApi>;
86
87
  /** Resolve a UPC/EAN barcode to the public-API album (with its `tracks`). */
87
88
  albumByUPC(upc: string): Promise<albumTypePublicApi>;
89
+ /** The 30-second preview clip URL for a track (plain MP3 — no licence, no decryption). */
90
+ trackPreview(track: trackType | string | number): Promise<TrackPreview | null>;
91
+ /** Fetch a track's 30-second preview clip as a `Buffer` (plain MP3). */
92
+ downloadPreview(track: trackType | string | number): Promise<Buffer | null>;
88
93
  /** Return a fully tagged audio Buffer for a track (nothing written to disk). */
89
94
  getTrackBuffer(track: trackType, quality?: Quality, options?: {}): Promise<Buffer | null>;
90
95
  /** Download a single track to disk. */
@@ -116,6 +116,14 @@ class Session {
116
116
  albumByUPC(upc) {
117
117
  return (0, gerdur_core_1.getAlbumByUPC)(upc);
118
118
  }
119
+ /** The 30-second preview clip URL for a track (plain MP3 — no licence, no decryption). */
120
+ trackPreview(track) {
121
+ return (0, gerdur_core_1.getTrackPreview)(track);
122
+ }
123
+ /** Fetch a track's 30-second preview clip as a `Buffer` (plain MP3). */
124
+ downloadPreview(track) {
125
+ return (0, gerdur_core_1.downloadPreview)(track);
126
+ }
119
127
  /** Return a fully tagged audio Buffer for a track (nothing written to disk). */
120
128
  getTrackBuffer(track, quality = '320', options = {}) {
121
129
  return (0, api_download_1.getTrackBuffer)(track, quality, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "The crucial streaming module for dabox systems.",
5
5
  "author": "Christian",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -48,7 +48,7 @@
48
48
  "adm-zip": "^0.5.16",
49
49
  "chalk": "^4.1.2",
50
50
  "commander": "^9.5.0",
51
- "gerdur-core": "^2.2.0",
51
+ "gerdur-core": "^2.3.0",
52
52
  "dot-prop": "^6.0.1",
53
53
  "got": "^11.8.6",
54
54
  "gradient-string": "^2.0.2",