gerdur 2.1.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 +25 -0
- package/README.md +63 -8
- package/dist/package.json +1 -1
- package/dist/src/gerdur.js +101 -13
- package/dist/src/lib/search.d.ts +39 -0
- package/dist/src/lib/search.js +67 -0
- package/dist/src/lib/session.d.ts +5 -3
- package/dist/src/lib/session.js +6 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
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
|
+
|
|
3
28
|
## 2.1.0 - 2026-08-31
|
|
4
29
|
|
|
5
30
|
### Added
|
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
|
-
|
|
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
|
-
|
|
|
141
|
-
|
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
| `
|
|
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
|
|
|
@@ -189,8 +240,12 @@ Session methods: `parseUrl`, `search`, `searchAdvanced`, `suggest`, `getUser`,
|
|
|
189
240
|
call is silent; `downloadTracks` / `downloadUrl` accept `concurrency` and an
|
|
190
241
|
`onProgress` callback and return one `{path, written} | null` per track.
|
|
191
242
|
|
|
192
|
-
`searchAdvanced`
|
|
193
|
-
`
|
|
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.
|
|
194
249
|
|
|
195
250
|
### Low-level: primitives
|
|
196
251
|
|
package/dist/package.json
CHANGED
package/dist/src/gerdur.js
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
if (!url.
|
|
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(
|
|
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
|
}
|
|
@@ -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,
|
|
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 {
|
|
@@ -58,9 +59,10 @@ 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
|
|
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?:
|
|
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>;
|
|
66
68
|
/** Return a fully tagged audio Buffer for a track (nothing written to disk). */
|
package/dist/src/lib/session.js
CHANGED
|
@@ -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,10 +64,12 @@ 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
|
|
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
|
-
|
|
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) {
|