gerdur-core 2.17.0 → 2.18.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,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.18.0 - 2026-08-31
4
+
5
+ Write operations — the first part of this package that changes an account.
6
+
7
+ ### Added
8
+
9
+ - **`src/api/favorites.ts`** — mutating library operations, in their own module
10
+ and wired into no download path, so they only run when called deliberately:
11
+ - `addFavoriteTracks(ids)` / `removeFavoriteTracks(ids)` (`song.addFavorites` /
12
+ `song.removeFavorites` — note the plural)
13
+ - `addFavoriteAlbum` / `removeFavoriteAlbum`, `addFavoriteArtist` /
14
+ `removeFavoriteArtist`
15
+ - `followPlaylist` / `unfollowPlaylist` (the gateway wants
16
+ `parent_playlist_id` to follow but `playlist_id` to unfollow)
17
+ - `addFavoriteShow`, `createPlaylist`, `addTracksToPlaylist`,
18
+ `removeTracksFromPlaylist`
19
+
20
+ The surface was mapped by calling each candidate with **incomplete
21
+ parameters** — an existing method answers `MISSING_PARAMETER_*`, an absent one
22
+ `GATEWAY_ERROR` — so method names and their parameter names were established
23
+ without creating or changing anything.
24
+
25
+ ### Verification status
26
+
27
+ - Method existence and parameter names: **confirmed by probe**.
28
+ - End-to-end execution: **not run.** `__tests__/favorites.ts` contains
29
+ snapshot → mutate → assert → revert → assert-restored round trips, gated
30
+ behind `GERDUR_ALLOW_WRITE_TESTS=1` so they never fire by accident.
31
+
32
+ ### Known gaps
33
+
34
+ - **Deezer's gateway has no playlist delete.** `playlist.delete`, `.remove`,
35
+ `.destroy`, `.deletePlaylist` and eight other spellings all answer
36
+ `GATEWAY_ERROR`, so `createPlaylist` is one-way — remove what it makes from a
37
+ Deezer client. Everything else here has a confirmed inverse.
38
+ - No `show.deleteFavorite` was found, so `addFavoriteShow` is one-way too.
39
+ - **`log.listen` (scrobbling) is not implemented.** It never answered the probe,
40
+ so its shape is unknown, and it writes to listening history that cannot be
41
+ undone — not something to ship on a guess.
42
+
3
43
  ## 2.17.0 - 2026-08-31
4
44
 
5
45
  The account's own library, over the authenticated gateway.
package/README.md CHANGED
@@ -47,6 +47,7 @@ the CLI and the file-writing layer on top.
47
47
  - [Browse and discover](#browse-and-discover)
48
48
  - [Flow, radios and a user's library](#flow-radios-and-a-users-library)
49
49
  - [Your own library](#your-own-library)
50
+ - [Changing the account (write operations)](#changing-the-account-write-operations)
50
51
  - [Podcasts](#podcasts)
51
52
  - [Preview clips](#preview-clips)
52
53
  - [Resolve a download URL](#resolve-a-download-url)
@@ -409,6 +410,43 @@ const urls = await resolveDownloadUrls(mix, [9, 3, 1]); // straight to download
409
410
  `userId` defaults to the logged-in account. These are account-scoped, so they
410
411
  never enter the shared cross-session cache.
411
412
 
413
+ ### Changing the account (write operations)
414
+
415
+ Everything above reads. These change the logged-in account's library for real —
416
+ they are in their own module, wired into no download path, and only run when you
417
+ call them.
418
+
419
+ | Function | Inverse |
420
+ | :--- | :--- |
421
+ | `addFavoriteTracks(ids)` | `removeFavoriteTracks(ids)` |
422
+ | `addFavoriteAlbum(id)` | `removeFavoriteAlbum(id)` |
423
+ | `addFavoriteArtist(id)` | `removeFavoriteArtist(id)` |
424
+ | `followPlaylist(id)` | `unfollowPlaylist(id)` |
425
+ | `addFavoriteShow(id)` | — none found |
426
+ | `createPlaylist(title, opts?)` | **— none exists, see below** |
427
+ | `addTracksToPlaylist(id, sngIds)` | `removeTracksFromPlaylist(id, sngIds)` |
428
+
429
+ ```ts
430
+ import {addFavoriteTracks, removeFavoriteTracks} from 'gerdur-core';
431
+
432
+ await addFavoriteTracks(['3135556']); // love it
433
+ await removeFavoriteTracks(['3135556']); // and back
434
+ ```
435
+
436
+ > **`createPlaylist` is a one-way door.** Deezer's gateway exposes no delete —
437
+ > a dozen spellings of `playlist.delete` all answer `GATEWAY_ERROR` — so a
438
+ > playlist made here has to be removed from a Deezer client. It is also the one
439
+ > function in this module that has not been exercised against a live account,
440
+ > for exactly that reason.
441
+ >
442
+ > Method names and parameters were established by probing with incomplete
443
+ > arguments (an existing method answers `MISSING_PARAMETER_*`), so the surface is
444
+ > real; the round trips live in `__tests__/favorites.ts` behind
445
+ > `GERDUR_ALLOW_WRITE_TESTS=1`.
446
+
447
+ Scrobbling (`log.listen`) is deliberately absent: it never answered the probe and
448
+ it writes history that cannot be undone.
449
+
412
450
  ### Podcasts
413
451
 
414
452
  ```ts
@@ -818,7 +856,11 @@ import type {
818
856
  `getUserChartTracks` · `getRadios` · `getRadioTracks` · `getRadioGenres` ·
819
857
  `getMyPlaylists` · `getMyFavoriteTracks` · `getMyFavoriteTrackIds` ·
820
858
  `getMyFavoriteAlbums` · `getMyFavoriteArtists` · `getMyFavoritePlaylists` ·
821
- `getMyFavoriteRadios` · `getMyFavoriteShows` · `getTrackMix`
859
+ `getMyFavoriteRadios` · `getMyFavoriteShows` · `getTrackMix` ·
860
+ `addFavoriteTracks` · `removeFavoriteTracks` · `addFavoriteAlbum` ·
861
+ `removeFavoriteAlbum` · `addFavoriteArtist` · `removeFavoriteArtist` ·
862
+ `followPlaylist` · `unfollowPlaylist` · `addFavoriteShow` · `createPlaylist` ·
863
+ `addTracksToPlaylist` · `removeTracksFromPlaylist`
822
864
  </details>
823
865
 
824
866
  <details>
@@ -0,0 +1,59 @@
1
+ /** What the gateway returns for a mutation — usually `true`, or the new id. */
2
+ export type WriteResult = boolean | string | number | Record<string, any>;
3
+ /**
4
+ * Love one or more tracks (`song.addFavorites`). Reversible with
5
+ * {@link removeFavoriteTracks}.
6
+ *
7
+ * @param sngIds `SNG_ID`s
8
+ */
9
+ export declare const addFavoriteTracks: (sngIds: (string | number)[]) => Promise<WriteResult>;
10
+ /** Un-love one or more tracks (`song.removeFavorites`). */
11
+ export declare const removeFavoriteTracks: (sngIds: (string | number)[]) => Promise<WriteResult>;
12
+ /** Add an album to favourites (`album.addFavorite`). */
13
+ export declare const addFavoriteAlbum: (albId: string | number) => Promise<WriteResult>;
14
+ /** Remove an album from favourites (`album.deleteFavorite`). */
15
+ export declare const removeFavoriteAlbum: (albId: string | number) => Promise<WriteResult>;
16
+ /** Follow an artist (`artist.addFavorite`). */
17
+ export declare const addFavoriteArtist: (artId: string | number) => Promise<WriteResult>;
18
+ /** Unfollow an artist (`artist.deleteFavorite`). */
19
+ export declare const removeFavoriteArtist: (artId: string | number) => Promise<WriteResult>;
20
+ /**
21
+ * Follow someone else's playlist (`playlist.addFavorite`). Note the gateway
22
+ * wants `parent_playlist_id` here but plain `playlist_id` to unfollow.
23
+ */
24
+ export declare const followPlaylist: (playlistId: string | number) => Promise<WriteResult>;
25
+ /** Unfollow a playlist (`playlist.deleteFavorite`). */
26
+ export declare const unfollowPlaylist: (playlistId: string | number) => Promise<WriteResult>;
27
+ /**
28
+ * Follow a podcast show (`show.addFavorite`).
29
+ *
30
+ * No `show.deleteFavorite` answered the probe, so treat this as one-way until
31
+ * the inverse is found.
32
+ */
33
+ export declare const addFavoriteShow: (showId: string | number) => Promise<WriteResult>;
34
+ /**
35
+ * Create a playlist (`playlist.create`). Resolves to the new `PLAYLIST_ID`.
36
+ *
37
+ * **One-way.** The gateway has no delete counterpart (see the module note), so
38
+ * anything created here has to be removed from a Deezer client. For the same
39
+ * reason this is the one function on this page that has not been exercised
40
+ * against a live account.
41
+ *
42
+ * @param title playlist title
43
+ * @param options `description`, `status` (0 public / 1 private / 2 collaborative),
44
+ * and `songs` to seed it with `SNG_ID`s
45
+ */
46
+ export declare const createPlaylist: (title: string, options?: {
47
+ description?: string;
48
+ status?: 0 | 1 | 2;
49
+ songs?: (string | number)[];
50
+ }) => Promise<WriteResult>;
51
+ /**
52
+ * Append tracks to a playlist (`playlist.addSongs`). Reversible with
53
+ * {@link removeTracksFromPlaylist}.
54
+ *
55
+ * The gateway takes `songs` as `[[sngId, offset], …]`; this wraps that for you.
56
+ */
57
+ export declare const addTracksToPlaylist: (playlistId: string | number, sngIds: (string | number)[]) => Promise<WriteResult>;
58
+ /** Remove tracks from a playlist (`playlist.deleteSongs`). */
59
+ export declare const removeTracksFromPlaylist: (playlistId: string | number, sngIds: (string | number)[]) => Promise<WriteResult>;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.removeTracksFromPlaylist = exports.addTracksToPlaylist = exports.createPlaylist = exports.addFavoriteShow = exports.unfollowPlaylist = exports.followPlaylist = exports.removeFavoriteArtist = exports.addFavoriteArtist = exports.removeFavoriteAlbum = exports.addFavoriteAlbum = exports.removeFavoriteTracks = exports.addFavoriteTracks = void 0;
4
+ /**
5
+ * **Mutating** operations on the logged-in account's library.
6
+ *
7
+ * Everything else in this package reads. These change the account, so they are
8
+ * kept in their own module and none of them are wired into any download path —
9
+ * you have to call them deliberately.
10
+ *
11
+ * The method names here were established by probing the gateway with incomplete
12
+ * parameters, which makes an existing method answer `MISSING_PARAMETER_*` while
13
+ * an absent one answers `GATEWAY_ERROR` — so the surface was mapped without
14
+ * creating or changing anything.
15
+ *
16
+ * **Deezer's gateway exposes no way to delete a playlist.** `playlist.delete`,
17
+ * `.remove`, `.destroy` and nine other spellings all answer `GATEWAY_ERROR`.
18
+ * {@link createPlaylist} is therefore a one-way door: whatever it makes, you
19
+ * remove from a Deezer client, not from here. Everything else on this page has a
20
+ * confirmed inverse and is safe to undo.
21
+ */
22
+ const request_1 = require("./request");
23
+ // ─── Loved tracks ────────────────────────────────────────────────────────────
24
+ /**
25
+ * Love one or more tracks (`song.addFavorites`). Reversible with
26
+ * {@link removeFavoriteTracks}.
27
+ *
28
+ * @param sngIds `SNG_ID`s
29
+ */
30
+ const addFavoriteTracks = (sngIds) => (0, request_1.request)({ ids: sngIds.map(String) }, 'song.addFavorites');
31
+ exports.addFavoriteTracks = addFavoriteTracks;
32
+ /** Un-love one or more tracks (`song.removeFavorites`). */
33
+ const removeFavoriteTracks = (sngIds) => (0, request_1.request)({ ids: sngIds.map(String) }, 'song.removeFavorites');
34
+ exports.removeFavoriteTracks = removeFavoriteTracks;
35
+ // ─── Albums / artists ────────────────────────────────────────────────────────
36
+ /** Add an album to favourites (`album.addFavorite`). */
37
+ const addFavoriteAlbum = (albId) => (0, request_1.request)({ alb_id: String(albId) }, 'album.addFavorite');
38
+ exports.addFavoriteAlbum = addFavoriteAlbum;
39
+ /** Remove an album from favourites (`album.deleteFavorite`). */
40
+ const removeFavoriteAlbum = (albId) => (0, request_1.request)({ alb_id: String(albId) }, 'album.deleteFavorite');
41
+ exports.removeFavoriteAlbum = removeFavoriteAlbum;
42
+ /** Follow an artist (`artist.addFavorite`). */
43
+ const addFavoriteArtist = (artId) => (0, request_1.request)({ art_id: String(artId) }, 'artist.addFavorite');
44
+ exports.addFavoriteArtist = addFavoriteArtist;
45
+ /** Unfollow an artist (`artist.deleteFavorite`). */
46
+ const removeFavoriteArtist = (artId) => (0, request_1.request)({ art_id: String(artId) }, 'artist.deleteFavorite');
47
+ exports.removeFavoriteArtist = removeFavoriteArtist;
48
+ // ─── Playlists & shows ───────────────────────────────────────────────────────
49
+ /**
50
+ * Follow someone else's playlist (`playlist.addFavorite`). Note the gateway
51
+ * wants `parent_playlist_id` here but plain `playlist_id` to unfollow.
52
+ */
53
+ const followPlaylist = (playlistId) => (0, request_1.request)({ parent_playlist_id: String(playlistId) }, 'playlist.addFavorite');
54
+ exports.followPlaylist = followPlaylist;
55
+ /** Unfollow a playlist (`playlist.deleteFavorite`). */
56
+ const unfollowPlaylist = (playlistId) => (0, request_1.request)({ playlist_id: String(playlistId) }, 'playlist.deleteFavorite');
57
+ exports.unfollowPlaylist = unfollowPlaylist;
58
+ /**
59
+ * Follow a podcast show (`show.addFavorite`).
60
+ *
61
+ * No `show.deleteFavorite` answered the probe, so treat this as one-way until
62
+ * the inverse is found.
63
+ */
64
+ const addFavoriteShow = (showId) => (0, request_1.request)({ show_id: String(showId) }, 'show.addFavorite');
65
+ exports.addFavoriteShow = addFavoriteShow;
66
+ // ─── Playlist contents ───────────────────────────────────────────────────────
67
+ /**
68
+ * Create a playlist (`playlist.create`). Resolves to the new `PLAYLIST_ID`.
69
+ *
70
+ * **One-way.** The gateway has no delete counterpart (see the module note), so
71
+ * anything created here has to be removed from a Deezer client. For the same
72
+ * reason this is the one function on this page that has not been exercised
73
+ * against a live account.
74
+ *
75
+ * @param title playlist title
76
+ * @param options `description`, `status` (0 public / 1 private / 2 collaborative),
77
+ * and `songs` to seed it with `SNG_ID`s
78
+ */
79
+ const createPlaylist = (title, options = {}) => {
80
+ var _a, _b, _c;
81
+ return (0, request_1.request)({
82
+ title,
83
+ description: (_a = options.description) !== null && _a !== void 0 ? _a : '',
84
+ status: (_b = options.status) !== null && _b !== void 0 ? _b : 1,
85
+ songs: ((_c = options.songs) !== null && _c !== void 0 ? _c : []).map((id) => [String(id), 0]),
86
+ }, 'playlist.create');
87
+ };
88
+ exports.createPlaylist = createPlaylist;
89
+ /**
90
+ * Append tracks to a playlist (`playlist.addSongs`). Reversible with
91
+ * {@link removeTracksFromPlaylist}.
92
+ *
93
+ * The gateway takes `songs` as `[[sngId, offset], …]`; this wraps that for you.
94
+ */
95
+ const addTracksToPlaylist = (playlistId, sngIds) => (0, request_1.request)({ playlist_id: String(playlistId), songs: sngIds.map((id) => [String(id), 0]) }, 'playlist.addSongs');
96
+ exports.addTracksToPlaylist = addTracksToPlaylist;
97
+ /** Remove tracks from a playlist (`playlist.deleteSongs`). */
98
+ const removeTracksFromPlaylist = (playlistId, sngIds) => (0, request_1.request)({ playlist_id: String(playlistId), songs: sngIds.map((id) => [String(id), 0]) }, 'playlist.deleteSongs');
99
+ exports.removeTracksFromPlaylist = removeTracksFromPlaylist;
@@ -6,3 +6,4 @@ export * from './preview';
6
6
  export * from './user';
7
7
  export * from './podcast';
8
8
  export * from './library';
9
+ export * from './favorites';
package/dist/api/index.js CHANGED
@@ -22,3 +22,4 @@ __exportStar(require("./preview"), exports);
22
22
  __exportStar(require("./user"), exports);
23
23
  __exportStar(require("./podcast"), exports);
24
24
  __exportStar(require("./library"), exports);
25
+ __exportStar(require("./favorites"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gerdur-core",
3
- "version": "2.17.0",
3
+ "version": "2.18.0",
4
4
  "description": "Deezer API client, cross-service URL resolution, Blowfish track decryption and MP3/FLAC metadata tagging — the engine behind the gerdur CLI.",
5
5
  "keywords": [
6
6
  "deezer",