gerdur 2.1.0 → 2.3.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,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.0 - 2026-08-31
4
+
5
+ ### Added
6
+
7
+ - **`gerdur-core@^2.2.0`** — the browse / discovery surface is re-exported:
8
+ `getGenres`, `getChart`, `getChartTracks`, `getGenreArtists`,
9
+ `getEditorialList` / `getEditorialReleases` / `getEditorialSelection` /
10
+ `getEditorialCharts`, `getArtistTopTracks`, `getRelatedArtists`,
11
+ `getArtistAlbums`, `getArtistPlaylists`, `getArtistRadioTracks`,
12
+ `getTrackByISRC`, `getAlbumByUPC`, plus their types.
13
+ - **`Session` browse methods**: `genres`, `chart`, `chartTracks`,
14
+ `editorialSections`, `artistTopTracks`, `relatedArtists`, `artistAlbums`,
15
+ `artistRadio`, `trackByISRC`, `albumByUPC`.
16
+ - **CLI `isrc:` / `upc:` prefixes** — `gerdur -u isrc:GBDUW0000059` downloads the
17
+ exact track for an ISRC; `gerdur -u upc:0724384960650` downloads the album for
18
+ a barcode. Both work in `--headless` mode and from the interactive prompt.
19
+
20
+ ## 2.2.0 - 2026-08-31
21
+
22
+ ### Added
23
+
24
+ - **CLI search.** New flags — `--search`, `--artist`, `--album`, `--track`,
25
+ `--label`, `--bpm-min` / `--bpm-max`, `--dur-min` / `--dur-max`,
26
+ `--search-limit` — are composed into a Deezer advanced-search query. The
27
+ matches are listed and you tick which to download; `--headless` grabs every
28
+ match up to `--search-limit`. Hits are resolved to full tracks before
29
+ downloading, so quality fallback / tagging / `.lrc` sidecars all work.
30
+ - **`search:` prefix** at the interactive prompt (and `-u 'search:…'`) runs the
31
+ same advanced track search, e.g. `search:artist:"daft punk" bpm_min:120`.
32
+ - When Deezer's advanced operators return nothing (they are unreliable), the CLI
33
+ automatically retries the same words as a plain free-text query.
34
+
35
+ ### Changed
36
+
37
+ - **`Session.searchAdvanced`** now falls back to a plain free-text query when the
38
+ advanced operators match nothing (opt out with `{fallback: false}`), and
39
+ accepts `fallback` in its options.
40
+ - The interactive prompt now reads "Enter a URL, a search term, or
41
+ `search:<advanced query>`".
42
+ - Search / filter helpers extracted to `src/lib/search.ts`
43
+ (`advancedFiltersFromFlags`, `searchAdvancedTracks`, `plainTextQuery`).
44
+
3
45
  ## 2.1.0 - 2026-08-31
4
46
 
5
47
  ### Added
package/README.md CHANGED
@@ -134,14 +134,69 @@ All options are optional. You can suppress prompts via providing `quality` and `
134
134
  | `--config-file` | `-conf` | Config location. Example: `gerdur -conf my-gerdur.config.json` | Specify custom location to config file |
135
135
  | `--update` | `-U` | *Nothing* | Download new update (binary only) |
136
136
  | `--help` | `-h` | *Nothing* | Shows the CLI help |
137
+ | `--search` | *None* | free-text query | Search tracks and pick what to download. Combine with the filters below. |
138
+ | `--artist` | *None* | artist name | Search filter — restrict results to this artist |
139
+ | `--album` | *None* | album title | Search filter — restrict results to this album |
140
+ | `--track` | *None* | track title | Search filter — restrict results to this title |
141
+ | `--label` | *None* | record label | Search filter — restrict results to this label |
142
+ | `--bpm-min` / `--bpm-max` | *None* | number | Search filter — tempo range (beats per minute) |
143
+ | `--dur-min` / `--dur-max` | *None* | number (seconds) | Search filter — track duration range |
144
+ | `--search-limit` | *None* | number (default 50) | How many search results to fetch |
145
+
146
+ ## Search
147
+
148
+ ### From the interactive prompt
149
+
150
+ When `gerdur` asks for a URL, a search term, or a prefixed query:
151
+
152
+ | Input | Does |
153
+ | :--- | :--- |
154
+ | `Harder Better Faster Stronger` | plain track search — pick tracks to download |
155
+ | `artist:daft punk` | artist search — pick an artist, then its discography |
156
+ | `album:discovery` | album search — pick an album |
157
+ | `playlist:deep focus` | playlist search — pick a playlist |
158
+ | `search:artist:"daft punk" bpm_min:120` | advanced track search (see operators below) |
159
+ | `isrc:GBDUW0000059` | download the exact track for an ISRC |
160
+ | `upc:0724384960650` | download the album for a UPC / EAN barcode |
161
+
162
+ `isrc:` and `upc:` also work in `--headless` mode (`-u isrc:…` / `-u upc:…`).
163
+
164
+ ### From flags (works headless)
165
+
166
+ The `--search` / `--artist` / `--album` / `--track` / `--label` / `--bpm-min` /
167
+ `--bpm-max` / `--dur-min` / `--dur-max` flags are composed into one Deezer
168
+ advanced-search query, the matches are listed, and — interactively — you pick
169
+ which to download. In `--headless` mode every match (up to `--search-limit`) is
170
+ downloaded.
171
+
172
+ ```bash
173
+ # interactive: search, then tick the tracks you want
174
+ gerdur --artist "Daft Punk" --track "Around the World"
175
+
176
+ # headless: grab the first 5 matches as FLAC
177
+ gerdur -d -q flac --search "get lucky" --artist "Daft Punk" --search-limit 5
178
+
179
+ # tempo / duration windows
180
+ gerdur --artist "Justice" --bpm-min 120 --bpm-max 130 --dur-min 180
181
+ ```
182
+
183
+ Search hits are resolved to full tracks (via `getTrackInfo`) before downloading,
184
+ so quality fallback, tagging and `.lrc` sidecars all work as normal.
137
185
 
138
- ## Search Parameters
186
+ ### Advanced query operators
139
187
 
140
- | Prefix | Description |
141
- | :---------: | :-------------: |
142
- | `artist:` | Search artist |
143
- | `album:` | Search album |
144
- | `playlist:` | Search playlist |
188
+ Usable inside `--search`, after `search:`, or via `buildAdvancedQuery` in code.
189
+ Deezer applies them as ranking hints (not hard filters), and they bite reliably
190
+ only on **track** search:
191
+
192
+ | Operator | Example |
193
+ | :--- | :--- |
194
+ | `artist:` | `artist:"daft punk"` |
195
+ | `album:` | `album:"discovery"` |
196
+ | `track:` | `track:"one more time"` |
197
+ | `label:` | `label:"Virgin"` |
198
+ | `dur_min:` / `dur_max:` | `dur_min:200` (seconds) |
199
+ | `bpm_min:` / `bpm_max:` | `bpm_min:120` |
145
200
 
146
201
  ## Programmatic API
147
202
 
@@ -184,13 +239,36 @@ const hits = await session.searchAdvanced(
184
239
  const suggestions = await session.suggest('daft'); // autocomplete
185
240
  ```
186
241
 
187
- Session methods: `parseUrl`, `search`, `searchAdvanced`, `suggest`, `getUser`,
188
- `getTrackBuffer`, `downloadTrack`, `downloadTracks`, `downloadUrl`. Every download
189
- call is silent; `downloadTracks` / `downloadUrl` accept `concurrency` and an
190
- `onProgress` callback and return one `{path, written} | null` per track.
242
+ Session methods:
243
+
244
+ - **resolve / search** `parseUrl`, `search`, `searchAdvanced`, `suggest`
245
+ - **browse** `genres`, `chart`, `chartTracks`, `editorialSections`,
246
+ `artistTopTracks`, `relatedArtists`, `artistAlbums`, `artistRadio`,
247
+ `trackByISRC`, `albumByUPC`
248
+ - **user / download** — `getUser`, `getTrackBuffer`, `downloadTrack`,
249
+ `downloadTracks`, `downloadUrl`
191
250
 
192
- `searchAdvanced` returns public-API track objects; fetch a hit's gw track with
193
- `getTrackInfo(id)` before downloading it.
251
+ Every download call is silent; `downloadTracks` / `downloadUrl` accept
252
+ `concurrency` and an `onProgress` callback and return one `{path, written} | null`
253
+ per track.
254
+
255
+ `searchAdvanced(filters, options?)` builds a Deezer advanced-search query from
256
+ `{query?, artist?, album?, track?, label?, durMin?, durMax?, bpmMin?, bpmMax?}`
257
+ and takes `{order?, strict?, limit?, index?, fallback?}`. Deezer's operators are
258
+ unreliable, so an empty result is retried as a plain free-text query unless
259
+ `fallback: false`. It returns public-API track objects — fetch a hit's gw track
260
+ with `getTrackInfo(id)` before downloading it.
261
+
262
+ ```ts
263
+ // Browse: this week's electro chart, download the top 5
264
+ const {tracks} = await session.chart(106, 5); // 106 = "Dance" genre
265
+ const full = await Promise.all(tracks.data.map((t) => getTrackInfo(String(t.id))));
266
+ await session.downloadTracks(full, 'flac');
267
+
268
+ // Find & grab an exact recording by barcode
269
+ const t = await session.trackByISRC('GBDUW0000059');
270
+ await session.downloadTrack(await getTrackInfo(String(t.id)), '320');
271
+ ```
194
272
 
195
273
  ### Low-level: primitives
196
274
 
@@ -214,11 +292,17 @@ const {path, written} = (await downloadTrackToFile(track, 'flac', {output: '{ART
214
292
  So you don't need `gerdur-core` as a second dependency (call after `initDeezerApi`
215
293
  or `createSession`):
216
294
 
217
- `parseInfo`, `searchMusic`, `searchPublicApi`, `searchTracks`, `searchAlbums`,
218
- `searchArtists`, `searchPlaylists`, `buildAdvancedQuery`, `suggest`, `getUser`,
219
- `getTrackInfo`, `getAlbumInfo`, `getAlbumTracks`, `getPlaylistInfo`,
220
- `getPlaylistTracks`, `getArtistInfo`, `getDiscography`, `getLyrics`,
221
- `getTrackDownloadUrl`, `resolveDownloadUrls`, `GeoBlocked`.
295
+ - **Query** — `parseInfo`, `getUser`, `getTrackInfo`, `getAlbumInfo`,
296
+ `getAlbumTracks`, `getPlaylistInfo`, `getPlaylistTracks`, `getArtistInfo`,
297
+ `getDiscography`, `getLyrics`
298
+ - **Search** — `searchMusic`, `searchPublicApi`, `searchTracks`, `searchAlbums`,
299
+ `searchArtists`, `searchPlaylists`, `buildAdvancedQuery`, `suggest`
300
+ - **Browse** — `getGenres`, `getChart`, `getChartTracks`, `getGenreArtists`,
301
+ `getEditorialList`, `getEditorialReleases`, `getEditorialSelection`,
302
+ `getEditorialCharts`, `getArtistTopTracks`, `getRelatedArtists`,
303
+ `getArtistAlbums`, `getArtistPlaylists`, `getArtistRadioTracks`,
304
+ `getTrackByISRC`, `getAlbumByUPC`
305
+ - **Download** — `getTrackDownloadUrl`, `resolveDownloadUrls`, `GeoBlocked`
222
306
 
223
307
  ### Auth & config helpers
224
308
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur",
3
- "version": "2.1.0",
3
+ "version": "2.3.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.1.0",
51
+ "gerdur-core": "^2.2.0",
52
52
  "dot-prop": "^6.0.1",
53
53
  "got": "^11.8.6",
54
54
  "gradient-string": "^2.0.2",
@@ -22,6 +22,7 @@ const update_check_1 = __importDefault(require("./lib/update-check"));
22
22
  const auto_updater_1 = __importDefault(require("./lib/auto-updater"));
23
23
  const arl_setup_1 = require("./lib/arl-setup");
24
24
  const util_1 = require("./lib/util");
25
+ const search_1 = require("./lib/search");
25
26
  const package_json_1 = __importDefault(require("../package.json"));
26
27
  // App info
27
28
  console.log((0, gradient_string_1.default)('red', 'yellow', 'orange')(` ♥ gerdur - ${package_json_1.default.version} ♥ `) +
@@ -46,7 +47,17 @@ const cmd = new commander_1.Command()
46
47
  .option('-d, --headless', 'Run in headless mode for scripting automation', false)
47
48
  .option('-conf, --config-file <file>', 'Custom location to your config file', 'gerdur.config.json')
48
49
  .option('-rfp, --resolve-full-path', 'Use absolute path for playlists')
49
- .option('-cp, --create-playlist', 'Force create a playlist file for non playlists');
50
+ .option('-cp, --create-playlist', 'Force create a playlist file for non playlists')
51
+ .option('--search <query>', 'Search tracks by free text (combine with the filters below)')
52
+ .option('--artist <name>', 'Search filter: artist name')
53
+ .option('--album <name>', 'Search filter: album title')
54
+ .option('--track <name>', 'Search filter: track title')
55
+ .option('--label <name>', 'Search filter: record label')
56
+ .option('--bpm-min <n>', 'Search filter: minimum BPM')
57
+ .option('--bpm-max <n>', 'Search filter: maximum BPM')
58
+ .option('--dur-min <seconds>', 'Search filter: minimum duration')
59
+ .option('--dur-max <seconds>', 'Search filter: maximum duration')
60
+ .option('--search-limit <n>', 'Max search results to fetch', '50');
50
61
  if (process.pkg) {
51
62
  cmd.option('-U, --update', 'Update this program to latest version');
52
63
  }
@@ -59,14 +70,23 @@ if (cmd.args[0] && cmd.args[0].toLowerCase() === 'setup') {
59
70
  if (!options.url && cmd.args[0]) {
60
71
  options.url = cmd.args[0];
61
72
  }
73
+ const toFiniteNumber = (value) => {
74
+ if (value === undefined || value === null || value === '') {
75
+ return undefined;
76
+ }
77
+ const n = Number(value);
78
+ return Number.isFinite(n) ? n : undefined;
79
+ };
80
+ const advancedFilters = (0, search_1.advancedFiltersFromFlags)(options);
81
+ let advancedFiltersConsumed = false;
62
82
  if (options.headless && !options.quality) {
63
83
  console.error(signale_1.default.error('Missing parameters --quality'));
64
84
  console.error(signale_1.default.note('Quality must be provided with headless mode'));
65
85
  process.exit(1);
66
86
  }
67
- if (options.headless && !options.url && !options.inputFile) {
87
+ if (options.headless && !options.url && !options.inputFile && !advancedFilters) {
68
88
  console.error(signale_1.default.error('Missing parameters --url'));
69
- console.error(signale_1.default.note('URL must be provided with headless mode'));
89
+ console.error(signale_1.default.note('Provide --url, --input-file, or a --search / --artist / … filter with headless mode'));
70
90
  process.exit(1);
71
91
  }
72
92
  const conf = new config_1.default(options.configFile);
@@ -79,8 +99,62 @@ const onCancel = () => {
79
99
  console.info(signale_1.default.note('Aborted!'));
80
100
  process.exit();
81
101
  };
102
+ const stampVersion = (t) => {
103
+ if (t.VERSION && !t.SNG_TITLE.includes(t.VERSION)) {
104
+ t.SNG_TITLE += ' ' + t.VERSION;
105
+ }
106
+ return t;
107
+ };
108
+ /**
109
+ * Run an advanced (public-REST) track search, let the user pick, and hydrate the
110
+ * picks into download-ready gw tracks via `getTrackInfo`. Returns `null` when
111
+ * nothing was matched or selected.
112
+ */
113
+ const resolveAdvancedSearch = async (filters) => {
114
+ const limit = toFiniteNumber(options.searchLimit) ?? 50;
115
+ const { data: results, query, usedFallback } = await (0, search_1.searchAdvancedTracks)(filters, { limit });
116
+ if (!query) {
117
+ return null;
118
+ }
119
+ console.log(signale_1.default.info(`Searching Deezer for: ${chalk_1.default.cyan(query)}`));
120
+ if (usedFallback) {
121
+ console.log(signale_1.default.note('No matches for the operators — retried as a plain-text search.'));
122
+ }
123
+ if (!results.length) {
124
+ console.log(signale_1.default.warn('No tracks matched that search.'));
125
+ return null;
126
+ }
127
+ let picks = results;
128
+ if (!options.headless) {
129
+ const choice = await (0, prompts_1.default)([
130
+ {
131
+ type: 'multiselect',
132
+ name: 'items',
133
+ message: `Select tracks to download. ${results.length} matched.`,
134
+ choices: results.map((r) => ({
135
+ title: `${r.title} — ${r.artist?.name ?? 'Unknown'}`,
136
+ value: r,
137
+ description: `Album: ${r.album?.title ?? 'Unknown'} · ${(0, util_1.formatSecondsReadable)(r.duration)}${r.explicit_lyrics ? ' · explicit' : ''}`,
138
+ })),
139
+ },
140
+ ], { onCancel });
141
+ picks = choice.items ?? [];
142
+ }
143
+ if (!picks.length) {
144
+ return null;
145
+ }
146
+ console.log(signale_1.default.info(`Fetching track data for ${picks.length} ${picks.length === 1 ? 'track' : 'tracks'}…`));
147
+ const hydrated = await Promise.all(picks.map((r) => (0, gerdur_core_1.getTrackInfo)(String(r.id)).catch(() => null)));
148
+ const tracks = hydrated.filter((t) => Boolean(t && t.SNG_ID)).map(stampVersion);
149
+ if (!tracks.length) {
150
+ console.log(signale_1.default.warn('None of the selected tracks could be resolved.'));
151
+ return null;
152
+ }
153
+ return { info: { type: 'track', id: query }, linktype: 'track', linkinfo: {}, tracks };
154
+ };
82
155
  const startDownload = async (saveLayout, url, skipPrompt) => {
83
156
  try {
157
+ url = url ?? '';
84
158
  if (!options.quality) {
85
159
  const { musicQuality } = await (0, prompts_1.default)([
86
160
  {
@@ -97,19 +171,57 @@ const startDownload = async (saveLayout, url, skipPrompt) => {
97
171
  ], { onCancel });
98
172
  options.quality = musicQuality;
99
173
  }
100
- if (!url) {
174
+ let searchData = null;
175
+ // `--search` / `--artist` / `--bpm-min` … flags: structured search, no URL needed.
176
+ // Consumed once — the interactive re-prompt loop must not re-run it.
177
+ if (!url && advancedFilters && !advancedFiltersConsumed) {
178
+ advancedFiltersConsumed = true;
179
+ searchData = await resolveAdvancedSearch(advancedFilters);
180
+ if (!searchData) {
181
+ if (options.headless) {
182
+ throw new Error('No tracks matched the search filters.');
183
+ }
184
+ return;
185
+ }
186
+ }
187
+ if (!searchData && !url) {
101
188
  const { query } = await (0, prompts_1.default)([
102
189
  {
103
190
  type: 'text',
104
191
  name: 'query',
105
- message: 'Enter URL or search:',
192
+ message: 'Enter a URL, a search term, or a `search:` / `isrc:` / `upc:` query:',
106
193
  validate: (value) => (value ? true : false),
107
194
  },
108
195
  ], { onCancel });
109
196
  url = query;
110
197
  }
111
- let searchData = null;
112
- if (!url.match(urlRegex)) {
198
+ // `search:` prefix — advanced track search from the prompt or `-u search:…`.
199
+ if (!searchData && url && url.startsWith('search:')) {
200
+ searchData = await resolveAdvancedSearch({ query: url.slice('search:'.length).trim() });
201
+ if (!searchData) {
202
+ throw new Error('No tracks matched that search.');
203
+ }
204
+ }
205
+ // `isrc:` / `upc:` — resolve a barcode to a Deezer track / album (works headless).
206
+ if (!searchData && url && url.startsWith('isrc:')) {
207
+ const code = url.slice('isrc:'.length).trim();
208
+ const found = await (0, gerdur_core_1.getTrackByISRC)(code).catch(() => null);
209
+ if (!found) {
210
+ throw new Error(`No Deezer track for ISRC ${code}`);
211
+ }
212
+ const track = await (0, gerdur_core_1.getTrackInfo)(String(found.id));
213
+ searchData = { info: { type: 'track', id: code }, linktype: 'track', linkinfo: {}, tracks: [stampVersion(track)] };
214
+ }
215
+ if (!searchData && url && url.startsWith('upc:')) {
216
+ const code = url.slice('upc:'.length).trim();
217
+ const found = await (0, gerdur_core_1.getAlbumByUPC)(code).catch(() => null);
218
+ if (!found) {
219
+ throw new Error(`No Deezer album for UPC ${code}`);
220
+ }
221
+ console.log(signale_1.default.info(`UPC ${code} → ${found.title} (${found.nb_tracks} tracks)`));
222
+ url = `https://www.deezer.com/album/${found.id}`;
223
+ }
224
+ if (!searchData && !url.match(urlRegex)) {
113
225
  if (options.headless) {
114
226
  throw new Error('Please provide a valid URL. Unknown URL: ' + url);
115
227
  }
@@ -168,12 +280,7 @@ const startDownload = async (saveLayout, url, skipPrompt) => {
168
280
  info: { type: 'track', id: url },
169
281
  linktype: 'track',
170
282
  linkinfo: {},
171
- tracks: TRACK.data.map((t) => {
172
- if (t.VERSION && !t.SNG_TITLE.includes(t.VERSION)) {
173
- t.SNG_TITLE += ' ' + t.VERSION;
174
- }
175
- return t;
176
- }),
283
+ tracks: TRACK.data.map(stampVersion),
177
284
  };
178
285
  }
179
286
  }
@@ -59,6 +59,6 @@ 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, 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, 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
- export type { advancedSearchFilters, searchOrder, searchEntity, publicApiSearchOptions, publicApiSearchResponse, searchResultTrack, searchResultAlbum, searchResultArtist, searchResultPlaylist, suggestResult, } from 'gerdur-core/types';
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';
package/dist/src/index.js CHANGED
@@ -33,7 +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.GeoBlocked = exports.toLrc = 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.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;
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;
37
38
  // ─── Authentication ────────────────────────────────────────────────────────
38
39
  const email_login_1 = require("./lib/email-login");
39
40
  Object.defineProperty(exports, "loginWithEmail", { enumerable: true, get: function () { return email_login_1.loginWithEmail; } });
@@ -96,6 +97,21 @@ Object.defineProperty(exports, "searchArtists", { enumerable: true, get: functio
96
97
  Object.defineProperty(exports, "searchPlaylists", { enumerable: true, get: function () { return gerdur_core_1.searchPlaylists; } });
97
98
  Object.defineProperty(exports, "buildAdvancedQuery", { enumerable: true, get: function () { return gerdur_core_1.buildAdvancedQuery; } });
98
99
  Object.defineProperty(exports, "suggest", { enumerable: true, get: function () { return gerdur_core_1.suggest; } });
100
+ Object.defineProperty(exports, "getGenres", { enumerable: true, get: function () { return gerdur_core_1.getGenres; } });
101
+ Object.defineProperty(exports, "getChart", { enumerable: true, get: function () { return gerdur_core_1.getChart; } });
102
+ Object.defineProperty(exports, "getChartTracks", { enumerable: true, get: function () { return gerdur_core_1.getChartTracks; } });
103
+ Object.defineProperty(exports, "getGenreArtists", { enumerable: true, get: function () { return gerdur_core_1.getGenreArtists; } });
104
+ Object.defineProperty(exports, "getEditorialList", { enumerable: true, get: function () { return gerdur_core_1.getEditorialList; } });
105
+ Object.defineProperty(exports, "getEditorialReleases", { enumerable: true, get: function () { return gerdur_core_1.getEditorialReleases; } });
106
+ Object.defineProperty(exports, "getEditorialSelection", { enumerable: true, get: function () { return gerdur_core_1.getEditorialSelection; } });
107
+ Object.defineProperty(exports, "getEditorialCharts", { enumerable: true, get: function () { return gerdur_core_1.getEditorialCharts; } });
108
+ Object.defineProperty(exports, "getArtistTopTracks", { enumerable: true, get: function () { return gerdur_core_1.getArtistTopTracks; } });
109
+ Object.defineProperty(exports, "getRelatedArtists", { enumerable: true, get: function () { return gerdur_core_1.getRelatedArtists; } });
110
+ Object.defineProperty(exports, "getArtistAlbums", { enumerable: true, get: function () { return gerdur_core_1.getArtistAlbums; } });
111
+ Object.defineProperty(exports, "getArtistPlaylists", { enumerable: true, get: function () { return gerdur_core_1.getArtistPlaylists; } });
112
+ Object.defineProperty(exports, "getArtistRadioTracks", { enumerable: true, get: function () { return gerdur_core_1.getArtistRadioTracks; } });
113
+ Object.defineProperty(exports, "getTrackByISRC", { enumerable: true, get: function () { return gerdur_core_1.getTrackByISRC; } });
114
+ Object.defineProperty(exports, "getAlbumByUPC", { enumerable: true, get: function () { return gerdur_core_1.getAlbumByUPC; } });
99
115
  Object.defineProperty(exports, "getUser", { enumerable: true, get: function () { return gerdur_core_1.getUser; } });
100
116
  Object.defineProperty(exports, "getTrackInfo", { enumerable: true, get: function () { return gerdur_core_1.getTrackInfo; } });
101
117
  Object.defineProperty(exports, "getAlbumInfo", { enumerable: true, get: function () { return gerdur_core_1.getAlbumInfo; } });
@@ -0,0 +1,39 @@
1
+ import type { advancedSearchFilters, publicApiSearchOptions, publicApiSearchResponse, searchResultTrack } from 'gerdur-core/types';
2
+ /** The raw `--search` / `--artist` / `--bpm-min` … values as `commander` parses them (strings). */
3
+ export interface SearchFlags {
4
+ search?: string;
5
+ artist?: string;
6
+ album?: string;
7
+ track?: string;
8
+ label?: string;
9
+ bpmMin?: string | number;
10
+ bpmMax?: string | number;
11
+ durMin?: string | number;
12
+ durMax?: string | number;
13
+ }
14
+ /**
15
+ * Fold the CLI search flags into a {@link advancedSearchFilters} bag for
16
+ * `buildAdvancedQuery` / `searchTracks`. Returns `null` when none were supplied,
17
+ * so the caller can tell "no search requested" from "empty search".
18
+ */
19
+ export declare const advancedFiltersFromFlags: (flags: SearchFlags) => advancedSearchFilters | null;
20
+ /** The words in a filter bag, for a free-text retry when the operators come back empty. */
21
+ export declare const plainTextQuery: (filters: advancedSearchFilters) => string;
22
+ export interface AdvancedSearchOptions extends Omit<publicApiSearchOptions, 'type'> {
23
+ /**
24
+ * Deezer's advanced operators are unreliable. When they return nothing, retry
25
+ * with the same words as a plain free-text query. Default `true`.
26
+ */
27
+ fallback?: boolean;
28
+ }
29
+ export interface AdvancedSearchResult extends publicApiSearchResponse<searchResultTrack> {
30
+ /** the advanced-operator query that was built */
31
+ query: string;
32
+ /** whether the free-text fallback was used */
33
+ usedFallback: boolean;
34
+ }
35
+ /**
36
+ * `buildAdvancedQuery` + `searchTracks`, with an automatic free-text retry when
37
+ * the operators match nothing (unless `fallback: false`).
38
+ */
39
+ export declare const searchAdvancedTracks: (filters: advancedSearchFilters, options?: AdvancedSearchOptions) => Promise<AdvancedSearchResult>;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.searchAdvancedTracks = exports.plainTextQuery = exports.advancedFiltersFromFlags = void 0;
4
+ const gerdur_core_1 = require("gerdur-core");
5
+ const toFiniteNumber = (value) => {
6
+ if (value === undefined || value === null || value === '') {
7
+ return undefined;
8
+ }
9
+ const n = Number(value);
10
+ return Number.isFinite(n) ? n : undefined;
11
+ };
12
+ const trimmed = (value) => {
13
+ if (typeof value !== 'string') {
14
+ return undefined;
15
+ }
16
+ const s = value.trim();
17
+ return s ? s : undefined;
18
+ };
19
+ /**
20
+ * Fold the CLI search flags into a {@link advancedSearchFilters} bag for
21
+ * `buildAdvancedQuery` / `searchTracks`. Returns `null` when none were supplied,
22
+ * so the caller can tell "no search requested" from "empty search".
23
+ */
24
+ const advancedFiltersFromFlags = (flags) => {
25
+ const filters = {
26
+ query: trimmed(flags.search),
27
+ artist: trimmed(flags.artist),
28
+ album: trimmed(flags.album),
29
+ track: trimmed(flags.track),
30
+ label: trimmed(flags.label),
31
+ durMin: toFiniteNumber(flags.durMin),
32
+ durMax: toFiniteNumber(flags.durMax),
33
+ bpmMin: toFiniteNumber(flags.bpmMin),
34
+ bpmMax: toFiniteNumber(flags.bpmMax),
35
+ };
36
+ return Object.values(filters).some((v) => v !== undefined) ? filters : null;
37
+ };
38
+ exports.advancedFiltersFromFlags = advancedFiltersFromFlags;
39
+ /** The words in a filter bag, for a free-text retry when the operators come back empty. */
40
+ const plainTextQuery = (filters) => [filters.query, filters.artist, filters.album, filters.track, filters.label]
41
+ .filter((v) => Boolean(v))
42
+ .join(' ')
43
+ .trim();
44
+ exports.plainTextQuery = plainTextQuery;
45
+ /**
46
+ * `buildAdvancedQuery` + `searchTracks`, with an automatic free-text retry when
47
+ * the operators match nothing (unless `fallback: false`).
48
+ */
49
+ const searchAdvancedTracks = async (filters, options = {}) => {
50
+ const { fallback = true, ...searchOptions } = options;
51
+ const query = (0, gerdur_core_1.buildAdvancedQuery)(filters);
52
+ // `query` already contains every text field, so an empty query means nothing to search.
53
+ if (!query) {
54
+ return { data: [], total: 0, query, usedFallback: false };
55
+ }
56
+ let response = await (0, gerdur_core_1.searchTracks)(query, searchOptions);
57
+ let usedFallback = false;
58
+ if (!response.data.length && fallback) {
59
+ const plain = (0, exports.plainTextQuery)(filters);
60
+ if (plain && plain !== query) {
61
+ response = await (0, gerdur_core_1.searchTracks)(plain, searchOptions);
62
+ usedFallback = true;
63
+ }
64
+ }
65
+ return { ...response, query, usedFallback };
66
+ };
67
+ exports.searchAdvancedTracks = searchAdvancedTracks;
@@ -1,7 +1,8 @@
1
1
  /// <reference types="node" />
2
2
  import { parseInfo, searchMusic } from 'gerdur-core';
3
+ import type { AdvancedSearchOptions } from './search';
3
4
  import type { Quality, DownloadTrackOptions, DownloadResult } from './api-download';
4
- import type { trackType, userType, advancedSearchFilters, publicApiSearchOptions, publicApiSearchResponse, searchResultTrack, suggestResult } from 'gerdur-core/types';
5
+ import type { trackType, userType, advancedSearchFilters, publicApiSearchResponse, searchResultTrack, suggestResult, chartType, genreType, editorialType, artistAlbumResult, searchResultArtist, publicApiList, trackTypePublicApi, albumTypePublicApi } from 'gerdur-core/types';
5
6
  /** Search result categories accepted by {@link Session.search}. */
6
7
  export type SearchType = 'ALBUM' | 'ARTIST' | 'TRACK' | 'PLAYLIST' | 'RADIO' | 'SHOW' | 'USER' | 'LIVESTREAM' | 'CHANNEL';
7
8
  export interface SessionOptions {
@@ -58,11 +59,32 @@ export declare class Session {
58
59
  * public-API track objects (with `isrc`, `preview`); to download a result,
59
60
  * fetch its gw track first with the re-exported `getTrackInfo(id)`.
60
61
  *
61
- * Deezer applies the operators loosely see `buildAdvancedQuery`.
62
+ * Deezer's advanced operators are unreliable, so an empty result is retried as
63
+ * a plain free-text query unless `options.fallback` is `false`.
62
64
  */
63
- searchAdvanced(filters: advancedSearchFilters, options?: Omit<publicApiSearchOptions, 'type'>): Promise<publicApiSearchResponse<searchResultTrack>>;
65
+ searchAdvanced(filters: advancedSearchFilters, options?: AdvancedSearchOptions): Promise<publicApiSearchResponse<searchResultTrack>>;
64
66
  /** `deezer.suggest` autocomplete — for "as you type" UIs. `nb` caps items per type. */
65
67
  suggest(query: string, nb?: number): Promise<suggestResult>;
68
+ /** Deezer's genre list (`id` `0` = "All"). */
69
+ genres(): Promise<publicApiList<genreType>>;
70
+ /** The five ranked lists for a genre (`0` = all): `{tracks, albums, artists, playlists, podcasts}`. */
71
+ chart(genreId?: number | string, limit?: number): Promise<chartType>;
72
+ /** Just the track chart for a genre — a ready-to-download list. */
73
+ chartTracks(genreId?: number | string, limit?: number, index?: number): Promise<publicApiList<searchResultTrack>>;
74
+ /** Deezer's editorial sections. */
75
+ editorialSections(): Promise<publicApiList<editorialType>>;
76
+ /** An artist's most popular tracks (public-API shape). */
77
+ artistTopTracks(artistId: number | string, limit?: number): Promise<publicApiList<searchResultTrack>>;
78
+ /** Artists Deezer considers related / similar. */
79
+ relatedArtists(artistId: number | string, limit?: number): Promise<publicApiList<searchResultArtist>>;
80
+ /** An artist's discography (public-API album shape). */
81
+ artistAlbums(artistId: number | string, limit?: number, index?: number): Promise<publicApiList<artistAlbumResult>>;
82
+ /** A ready-made radio (track list) seeded from an artist. */
83
+ artistRadio(artistId: number | string): Promise<publicApiList<searchResultTrack>>;
84
+ /** Resolve an ISRC to the public-API track. Pass `.id` to `getTrackInfo` to download. */
85
+ trackByISRC(isrc: string): Promise<trackTypePublicApi>;
86
+ /** Resolve a UPC/EAN barcode to the public-API album (with its `tracks`). */
87
+ albumByUPC(upc: string): Promise<albumTypePublicApi>;
66
88
  /** Return a fully tagged audio Buffer for a track (nothing written to disk). */
67
89
  getTrackBuffer(track: trackType, quality?: Quality, options?: {}): Promise<Buffer | null>;
68
90
  /** Download a single track to disk. */
@@ -8,6 +8,7 @@ const p_queue_1 = __importDefault(require("p-queue"));
8
8
  const gerdur_core_1 = require("gerdur-core");
9
9
  const email_login_1 = require("./email-login");
10
10
  const api_download_1 = require("./api-download");
11
+ const search_1 = require("./search");
11
12
  /**
12
13
  * An authenticated Deezer session exposing high-level query and download
13
14
  * helpers. Create one with {@link createSession}. All methods are silent (no
@@ -63,15 +64,58 @@ class Session {
63
64
  * public-API track objects (with `isrc`, `preview`); to download a result,
64
65
  * fetch its gw track first with the re-exported `getTrackInfo(id)`.
65
66
  *
66
- * Deezer applies the operators loosely see `buildAdvancedQuery`.
67
+ * Deezer's advanced operators are unreliable, so an empty result is retried as
68
+ * a plain free-text query unless `options.fallback` is `false`.
67
69
  */
68
- searchAdvanced(filters, options = {}) {
69
- return (0, gerdur_core_1.searchTracks)((0, gerdur_core_1.buildAdvancedQuery)(filters), options);
70
+ async searchAdvanced(filters, options = {}) {
71
+ const { query: _query, usedFallback: _usedFallback, ...response } = await (0, search_1.searchAdvancedTracks)(filters, options);
72
+ return response;
70
73
  }
71
74
  /** `deezer.suggest` autocomplete — for "as you type" UIs. `nb` caps items per type. */
72
75
  suggest(query, nb) {
73
76
  return (0, gerdur_core_1.suggest)(query, nb);
74
77
  }
78
+ // ─── Browse & discovery (public REST) ──────────────────────────────────────
79
+ /** Deezer's genre list (`id` `0` = "All"). */
80
+ genres() {
81
+ return (0, gerdur_core_1.getGenres)();
82
+ }
83
+ /** The five ranked lists for a genre (`0` = all): `{tracks, albums, artists, playlists, podcasts}`. */
84
+ chart(genreId = 0, limit = 10) {
85
+ return (0, gerdur_core_1.getChart)(genreId, limit);
86
+ }
87
+ /** Just the track chart for a genre — a ready-to-download list. */
88
+ chartTracks(genreId = 0, limit = 100, index = 0) {
89
+ return (0, gerdur_core_1.getChartTracks)(genreId, limit, index);
90
+ }
91
+ /** Deezer's editorial sections. */
92
+ editorialSections() {
93
+ return (0, gerdur_core_1.getEditorialList)();
94
+ }
95
+ /** An artist's most popular tracks (public-API shape). */
96
+ artistTopTracks(artistId, limit = 50) {
97
+ return (0, gerdur_core_1.getArtistTopTracks)(artistId, limit);
98
+ }
99
+ /** Artists Deezer considers related / similar. */
100
+ relatedArtists(artistId, limit = 20) {
101
+ return (0, gerdur_core_1.getRelatedArtists)(artistId, limit);
102
+ }
103
+ /** An artist's discography (public-API album shape). */
104
+ artistAlbums(artistId, limit = 50, index = 0) {
105
+ return (0, gerdur_core_1.getArtistAlbums)(artistId, limit, index);
106
+ }
107
+ /** A ready-made radio (track list) seeded from an artist. */
108
+ artistRadio(artistId) {
109
+ return (0, gerdur_core_1.getArtistRadioTracks)(artistId);
110
+ }
111
+ /** Resolve an ISRC to the public-API track. Pass `.id` to `getTrackInfo` to download. */
112
+ trackByISRC(isrc) {
113
+ return (0, gerdur_core_1.getTrackByISRC)(isrc);
114
+ }
115
+ /** Resolve a UPC/EAN barcode to the public-API album (with its `tracks`). */
116
+ albumByUPC(upc) {
117
+ return (0, gerdur_core_1.getAlbumByUPC)(upc);
118
+ }
75
119
  /** Return a fully tagged audio Buffer for a track (nothing written to disk). */
76
120
  getTrackBuffer(track, quality = '320', options = {}) {
77
121
  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.1.0",
3
+ "version": "2.3.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.1.0",
51
+ "gerdur-core": "^2.2.0",
52
52
  "dot-prop": "^6.0.1",
53
53
  "got": "^11.8.6",
54
54
  "gradient-string": "^2.0.2",