gerdur 2.0.0 → 2.2.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,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.2.0 - 2026-08-31
4
+
5
+ ### Added
6
+
7
+ - **CLI search.** New flags — `--search`, `--artist`, `--album`, `--track`,
8
+ `--label`, `--bpm-min` / `--bpm-max`, `--dur-min` / `--dur-max`,
9
+ `--search-limit` — are composed into a Deezer advanced-search query. The
10
+ matches are listed and you tick which to download; `--headless` grabs every
11
+ match up to `--search-limit`. Hits are resolved to full tracks before
12
+ downloading, so quality fallback / tagging / `.lrc` sidecars all work.
13
+ - **`search:` prefix** at the interactive prompt (and `-u 'search:…'`) runs the
14
+ same advanced track search, e.g. `search:artist:"daft punk" bpm_min:120`.
15
+ - When Deezer's advanced operators return nothing (they are unreliable), the CLI
16
+ automatically retries the same words as a plain free-text query.
17
+
18
+ ### Changed
19
+
20
+ - **`Session.searchAdvanced`** now falls back to a plain free-text query when the
21
+ advanced operators match nothing (opt out with `{fallback: false}`), and
22
+ accepts `fallback` in its options.
23
+ - The interactive prompt now reads "Enter a URL, a search term, or
24
+ `search:<advanced query>`".
25
+ - Search / filter helpers extracted to `src/lib/search.ts`
26
+ (`advancedFiltersFromFlags`, `searchAdvancedTracks`, `plainTextQuery`).
27
+
28
+ ## 2.1.0 - 2026-08-31
29
+
30
+ ### Added
31
+
32
+ - **`gerdur-core@^2.1.0`** — the new search surface is re-exported from the
33
+ library: `searchPublicApi`, `searchTracks` / `searchAlbums` / `searchArtists` /
34
+ `searchPlaylists`, `buildAdvancedQuery`, `suggest`, plus the matching types.
35
+ - **`Session.searchAdvanced(filters, options?)`** — structured search against the
36
+ public REST API — a free-text query plus `{artist, album, track, label, durMin,
37
+ durMax, bpmMin, bpmMax}`, with `order` / `limit` / `index`. Returns public-API
38
+ track objects with `isrc` / `preview`; fetch a hit's gw track with
39
+ `getTrackInfo(id)` to download.
40
+ - **`Session.suggest(query, nb?)`** — `deezer.suggest` autocomplete.
41
+
42
+ _No CLI changes in this release — the interactive `gerdur` flow is unchanged._
43
+
3
44
  ## 2.0.0 - 2026-08-31
4
45
 
5
46
  ### Changed
package/README.md CHANGED
@@ -134,14 +134,65 @@ 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 `search:<advanced 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
+
160
+ ### From flags (works headless)
161
+
162
+ The `--search` / `--artist` / `--album` / `--track` / `--label` / `--bpm-min` /
163
+ `--bpm-max` / `--dur-min` / `--dur-max` flags are composed into one Deezer
164
+ advanced-search query, the matches are listed, and — interactively — you pick
165
+ which to download. In `--headless` mode every match (up to `--search-limit`) is
166
+ downloaded.
167
+
168
+ ```bash
169
+ # interactive: search, then tick the tracks you want
170
+ gerdur --artist "Daft Punk" --track "Around the World"
171
+
172
+ # headless: grab the first 5 matches as FLAC
173
+ gerdur -d -q flac --search "get lucky" --artist "Daft Punk" --search-limit 5
174
+
175
+ # tempo / duration windows
176
+ gerdur --artist "Justice" --bpm-min 120 --bpm-max 130 --dur-min 180
177
+ ```
178
+
179
+ Search hits are resolved to full tracks (via `getTrackInfo`) before downloading,
180
+ so quality fallback, tagging and `.lrc` sidecars all work as normal.
181
+
182
+ ### Advanced query operators
137
183
 
138
- ## Search Parameters
184
+ Usable inside `--search`, after `search:`, or via `buildAdvancedQuery` in code.
185
+ Deezer applies them as ranking hints (not hard filters), and they bite reliably
186
+ only on **track** search:
139
187
 
140
- | Prefix | Description |
141
- | :---------: | :-------------: |
142
- | `artist:` | Search artist |
143
- | `album:` | Search album |
144
- | `playlist:` | Search playlist |
188
+ | Operator | Example |
189
+ | :--- | :--- |
190
+ | `artist:` | `artist:"daft punk"` |
191
+ | `album:` | `album:"discovery"` |
192
+ | `track:` | `track:"one more time"` |
193
+ | `label:` | `label:"Virgin"` |
194
+ | `dur_min:` / `dur_max:` | `dur_min:200` (seconds) |
195
+ | `bpm_min:` / `bpm_max:` | `bpm_min:120` |
145
196
 
146
197
  ## Programmatic API
147
198
 
@@ -175,12 +226,26 @@ await session.downloadUrl('https://deezer.com/album/302127', 'flac', {
175
226
  const {tracks} = await session.parseUrl('https://deezer.com/track/3135556');
176
227
  const found = await session.search('daft punk', ['TRACK'], 10);
177
228
  const results = await session.downloadTracks(tracks, '320', {output: '{ART_NAME} - {SNG_TITLE}'});
229
+
230
+ // Structured search against the public REST API (isrc / preview / bpm-aware):
231
+ const hits = await session.searchAdvanced(
232
+ {query: 'one more time', artist: 'daft punk', durMin: 200},
233
+ {limit: 25, order: 'RANKING'},
234
+ );
235
+ const suggestions = await session.suggest('daft'); // autocomplete
178
236
  ```
179
237
 
180
- Session methods: `parseUrl`, `search`, `getUser`, `getTrackBuffer`, `downloadTrack`,
181
- `downloadTracks`, `downloadUrl`. Every download call is silent; `downloadTracks` /
182
- `downloadUrl` accept `concurrency` and an `onProgress` callback and return one
183
- `{path, written} | null` per track.
238
+ Session methods: `parseUrl`, `search`, `searchAdvanced`, `suggest`, `getUser`,
239
+ `getTrackBuffer`, `downloadTrack`, `downloadTracks`, `downloadUrl`. Every download
240
+ call is silent; `downloadTracks` / `downloadUrl` accept `concurrency` and an
241
+ `onProgress` callback and return one `{path, written} | null` per track.
242
+
243
+ `searchAdvanced(filters, options?)` builds a Deezer advanced-search query from
244
+ `{query?, artist?, album?, track?, label?, durMin?, durMax?, bpmMin?, bpmMax?}`
245
+ and takes `{order?, strict?, limit?, index?, fallback?}`. Deezer's operators are
246
+ unreliable, so an empty result is retried as a plain free-text query unless
247
+ `fallback: false`. It returns public-API track objects — fetch a hit's gw track
248
+ with `getTrackInfo(id)` before downloading it.
184
249
 
185
250
  ### Low-level: primitives
186
251
 
@@ -204,9 +269,11 @@ const {path, written} = (await downloadTrackToFile(track, 'flac', {output: '{ART
204
269
  So you don't need `gerdur-core` as a second dependency (call after `initDeezerApi`
205
270
  or `createSession`):
206
271
 
207
- `parseInfo`, `searchMusic`, `getUser`, `getTrackInfo`, `getAlbumInfo`, `getAlbumTracks`,
208
- `getPlaylistInfo`, `getPlaylistTracks`, `getArtistInfo`, `getDiscography`, `getLyrics`,
209
- `getTrackDownloadUrl`, `GeoBlocked`.
272
+ `parseInfo`, `searchMusic`, `searchPublicApi`, `searchTracks`, `searchAlbums`,
273
+ `searchArtists`, `searchPlaylists`, `buildAdvancedQuery`, `suggest`, `getUser`,
274
+ `getTrackInfo`, `getAlbumInfo`, `getAlbumTracks`, `getPlaylistInfo`,
275
+ `getPlaylistTracks`, `getArtistInfo`, `getDiscography`, `getLyrics`,
276
+ `getTrackDownloadUrl`, `resolveDownloadUrls`, `GeoBlocked`.
210
277
 
211
278
  ### Auth & config helpers
212
279
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur",
3
- "version": "2.0.0",
3
+ "version": "2.2.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.0.0",
51
+ "gerdur-core": "^2.1.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,38 @@ 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 `search:<advanced 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
+ if (!searchData && !url.match(urlRegex)) {
113
206
  if (options.headless) {
114
207
  throw new Error('Please provide a valid URL. Unknown URL: ' + url);
115
208
  }
@@ -168,12 +261,7 @@ const startDownload = async (saveLayout, url, skipPrompt) => {
168
261
  info: { type: 'track', id: url },
169
262
  linktype: 'track',
170
263
  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
- }),
264
+ tracks: TRACK.data.map(stampVersion),
177
265
  };
178
266
  }
179
267
  }
@@ -59,5 +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, 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, 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';
package/dist/src/index.js CHANGED
@@ -33,7 +33,7 @@ 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.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.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;
37
37
  // ─── Authentication ────────────────────────────────────────────────────────
38
38
  const email_login_1 = require("./lib/email-login");
39
39
  Object.defineProperty(exports, "loginWithEmail", { enumerable: true, get: function () { return email_login_1.loginWithEmail; } });
@@ -89,6 +89,13 @@ var gerdur_core_1 = require("gerdur-core");
89
89
  Object.defineProperty(exports, "initDeezerApi", { enumerable: true, get: function () { return gerdur_core_1.initDeezerApi; } });
90
90
  Object.defineProperty(exports, "parseInfo", { enumerable: true, get: function () { return gerdur_core_1.parseInfo; } });
91
91
  Object.defineProperty(exports, "searchMusic", { enumerable: true, get: function () { return gerdur_core_1.searchMusic; } });
92
+ Object.defineProperty(exports, "searchPublicApi", { enumerable: true, get: function () { return gerdur_core_1.searchPublicApi; } });
93
+ Object.defineProperty(exports, "searchTracks", { enumerable: true, get: function () { return gerdur_core_1.searchTracks; } });
94
+ Object.defineProperty(exports, "searchAlbums", { enumerable: true, get: function () { return gerdur_core_1.searchAlbums; } });
95
+ Object.defineProperty(exports, "searchArtists", { enumerable: true, get: function () { return gerdur_core_1.searchArtists; } });
96
+ Object.defineProperty(exports, "searchPlaylists", { enumerable: true, get: function () { return gerdur_core_1.searchPlaylists; } });
97
+ Object.defineProperty(exports, "buildAdvancedQuery", { enumerable: true, get: function () { return gerdur_core_1.buildAdvancedQuery; } });
98
+ Object.defineProperty(exports, "suggest", { enumerable: true, get: function () { return gerdur_core_1.suggest; } });
92
99
  Object.defineProperty(exports, "getUser", { enumerable: true, get: function () { return gerdur_core_1.getUser; } });
93
100
  Object.defineProperty(exports, "getTrackInfo", { enumerable: true, get: function () { return gerdur_core_1.getTrackInfo; } });
94
101
  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 } from 'gerdur-core/types';
5
+ import type { trackType, userType, advancedSearchFilters, publicApiSearchResponse, searchResultTrack, suggestResult } 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 {
@@ -52,6 +53,18 @@ export declare class Session {
52
53
  parseUrl(url: string): ReturnType<typeof parseInfo>;
53
54
  /** Search Deezer. Defaults to track results. */
54
55
  search(query: string, types?: SearchType[], limit?: number): ReturnType<typeof searchMusic>;
56
+ /**
57
+ * Search the public REST API with structured filters (`{artist, album, track,
58
+ * label, durMin, durMax, bpmMin, bpmMax}`) plus a free-text `query`. Returns
59
+ * public-API track objects (with `isrc`, `preview`); to download a result,
60
+ * fetch its gw track first with the re-exported `getTrackInfo(id)`.
61
+ *
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`.
64
+ */
65
+ searchAdvanced(filters: advancedSearchFilters, options?: AdvancedSearchOptions): Promise<publicApiSearchResponse<searchResultTrack>>;
66
+ /** `deezer.suggest` autocomplete — for "as you type" UIs. `nb` caps items per type. */
67
+ suggest(query: string, nb?: number): Promise<suggestResult>;
55
68
  /** Return a fully tagged audio Buffer for a track (nothing written to disk). */
56
69
  getTrackBuffer(track: trackType, quality?: Quality, options?: {}): Promise<Buffer | null>;
57
70
  /** 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
@@ -57,6 +58,23 @@ class Session {
57
58
  search(query, types = ['TRACK'], limit) {
58
59
  return (0, gerdur_core_1.searchMusic)(query, types, limit);
59
60
  }
61
+ /**
62
+ * Search the public REST API with structured filters (`{artist, album, track,
63
+ * label, durMin, durMax, bpmMin, bpmMax}`) plus a free-text `query`. Returns
64
+ * public-API track objects (with `isrc`, `preview`); to download a result,
65
+ * fetch its gw track first with the re-exported `getTrackInfo(id)`.
66
+ *
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`.
69
+ */
70
+ async searchAdvanced(filters, options = {}) {
71
+ const { query: _query, usedFallback: _usedFallback, ...response } = await (0, search_1.searchAdvancedTracks)(filters, options);
72
+ return response;
73
+ }
74
+ /** `deezer.suggest` autocomplete — for "as you type" UIs. `nb` caps items per type. */
75
+ suggest(query, nb) {
76
+ return (0, gerdur_core_1.suggest)(query, nb);
77
+ }
60
78
  /** Return a fully tagged audio Buffer for a track (nothing written to disk). */
61
79
  getTrackBuffer(track, quality = '320', options = {}) {
62
80
  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.0.0",
3
+ "version": "2.2.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.0.0",
51
+ "gerdur-core": "^2.1.0",
52
52
  "dot-prop": "^6.0.1",
53
53
  "got": "^11.8.6",
54
54
  "gradient-string": "^2.0.2",