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 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
- ## 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
 
@@ -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` returns public-API track objects; fetch a hit's gw track with
193
- `getTrackInfo(id)` before downloading it.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur",
3
- "version": "2.1.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",
@@ -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
  }
@@ -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 } 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 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>;
66
68
  /** Return a fully tagged audio Buffer for a track (nothing written 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,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 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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur",
3
- "version": "2.1.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",