@lucaperret/tidal-cli 1.2.3 → 1.2.5
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/dist/history.js +8 -13
- package/dist/library.js +4 -11
- package/dist/pagination.d.ts +19 -0
- package/dist/pagination.js +86 -0
- package/dist/playlist.js +10 -22
- package/dist/saved.js +5 -9
- package/package.json +1 -1
package/dist/history.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getRecentlyAddedData = getRecentlyAddedData;
|
|
4
4
|
exports.getRecentlyAdded = getRecentlyAdded;
|
|
5
5
|
const auth_1 = require("./auth");
|
|
6
|
+
const pagination_1 = require("./pagination");
|
|
6
7
|
const endpointMap = {
|
|
7
8
|
tracks: '/userCollectionTracks/{id}/relationships/items',
|
|
8
9
|
albums: '/userCollectionAlbums/{id}/relationships/items',
|
|
@@ -14,20 +15,15 @@ const includeTypeMap = {
|
|
|
14
15
|
artists: 'artists',
|
|
15
16
|
};
|
|
16
17
|
async function getRecentlyAddedData(type, client, countryCode) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
18
|
+
// Paginated: returns the full collection (most-recent-first), not just the first ~20.
|
|
19
|
+
const { data: relData, included } = await (0, pagination_1.fetchAllPages)(client, endpointMap[type], {
|
|
20
|
+
path: { id: 'me' },
|
|
21
|
+
query: {
|
|
22
|
+
countryCode,
|
|
23
|
+
include: ['items'],
|
|
24
|
+
sort: ['-addedAt'],
|
|
25
25
|
},
|
|
26
26
|
});
|
|
27
|
-
if (error || !data) {
|
|
28
|
-
throw new Error(`Failed to get recently added ${type} — ${JSON.stringify(error)}`);
|
|
29
|
-
}
|
|
30
|
-
const included = data.included ?? [];
|
|
31
27
|
const items = included
|
|
32
28
|
.filter((item) => item.type === includeTypeMap[type])
|
|
33
29
|
.map((item) => {
|
|
@@ -39,7 +35,6 @@ async function getRecentlyAddedData(type, client, countryCode) {
|
|
|
39
35
|
};
|
|
40
36
|
});
|
|
41
37
|
// Enrich with addedAt from the relationship data if available
|
|
42
|
-
const relData = data.data ?? [];
|
|
43
38
|
for (const rel of relData) {
|
|
44
39
|
const addedAt = rel.meta?.addedAt;
|
|
45
40
|
if (addedAt) {
|
package/dist/library.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.addPlaylistToFavorites = addPlaylistToFavorites;
|
|
|
11
11
|
exports.removePlaylistFromFavoritesData = removePlaylistFromFavoritesData;
|
|
12
12
|
exports.removePlaylistFromFavorites = removePlaylistFromFavorites;
|
|
13
13
|
const auth_1 = require("./auth");
|
|
14
|
+
const pagination_1 = require("./pagination");
|
|
14
15
|
const collectionEndpoints = {
|
|
15
16
|
artist: { path: '/userCollectionArtists/{id}/relationships/items', type: 'artists' },
|
|
16
17
|
album: { path: '/userCollectionAlbums/{id}/relationships/items', type: 'albums' },
|
|
@@ -74,18 +75,10 @@ async function removeFromLibrary(resourceType, resourceId, json) {
|
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
async function listFavoritedPlaylistsData(client) {
|
|
77
|
-
const {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
query: {
|
|
81
|
-
include: ['items'],
|
|
82
|
-
},
|
|
83
|
-
},
|
|
78
|
+
const { included } = await (0, pagination_1.fetchAllPages)(client, '/userCollectionPlaylists/{id}/relationships/items', {
|
|
79
|
+
path: { id: 'me' },
|
|
80
|
+
query: { include: ['items'] },
|
|
84
81
|
});
|
|
85
|
-
if (error || !data) {
|
|
86
|
-
throw new Error(`Failed to list favorited playlists — ${JSON.stringify(error)}`);
|
|
87
|
-
}
|
|
88
|
-
const included = data.included ?? [];
|
|
89
82
|
return included
|
|
90
83
|
.filter((item) => item.type === 'playlists')
|
|
91
84
|
.map((item) => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Pull the `page[cursor]` token out of a JSON:API `links.next` value. */
|
|
2
|
+
export declare function extractCursor(next: unknown): string | undefined;
|
|
3
|
+
interface PageParams {
|
|
4
|
+
path?: Record<string, unknown>;
|
|
5
|
+
query?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
interface PaginationOptions {
|
|
8
|
+
/** Delay between per-page retries. Overridable (set to 0) to keep tests fast. */
|
|
9
|
+
retryDelayMs?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Repeatedly GET `path`, following `links.next`, until no further page.
|
|
13
|
+
* Returns the concatenated `data` and `included` arrays from every page.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fetchAllPages(client: any, path: string, params: PageParams, options?: PaginationOptions): Promise<{
|
|
16
|
+
data: any[];
|
|
17
|
+
included: any[];
|
|
18
|
+
}>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Cursor-based pagination for the Tidal v2 JSON:API.
|
|
3
|
+
//
|
|
4
|
+
// List responses return one page (~20 items) plus a `links.next` value carrying a
|
|
5
|
+
// `page[cursor]` token for the following page. The API also intermittently returns
|
|
6
|
+
// an HTTP 200 with an empty `data` array — a transient glitch that must be retried,
|
|
7
|
+
// otherwise a single bad response silently truncates the result.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.extractCursor = extractCursor;
|
|
10
|
+
exports.fetchAllPages = fetchAllPages;
|
|
11
|
+
const MAX_PAGES = 100; // safety cap against a server that never stops returning `next`
|
|
12
|
+
const MAX_ATTEMPTS = 3; // per-page attempts for transient empty/error responses
|
|
13
|
+
const RETRY_DELAY_MS = 250;
|
|
14
|
+
/** Pull the `page[cursor]` token out of a JSON:API `links.next` value. */
|
|
15
|
+
function extractCursor(next) {
|
|
16
|
+
if (typeof next !== 'string' || next.length === 0)
|
|
17
|
+
return undefined;
|
|
18
|
+
const queryStart = next.indexOf('?');
|
|
19
|
+
const queryString = queryStart >= 0 ? next.slice(queryStart + 1) : next;
|
|
20
|
+
const cursor = new URLSearchParams(queryString).get('page[cursor]');
|
|
21
|
+
return cursor ?? undefined;
|
|
22
|
+
}
|
|
23
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
/**
|
|
25
|
+
* GET a single page, retrying transient failures (an `{ error }` response or an
|
|
26
|
+
* HTTP 200 with an empty `data` array). Throws once every attempt has errored;
|
|
27
|
+
* returns the (possibly empty) page once retries are exhausted without an error —
|
|
28
|
+
* an empty page is then treated as genuine end-of-data.
|
|
29
|
+
*/
|
|
30
|
+
async function fetchPage(client, path, requestParams, retryDelayMs) {
|
|
31
|
+
let lastError;
|
|
32
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
33
|
+
const { data, error } = await client.GET(path, { params: requestParams });
|
|
34
|
+
if (!error && data) {
|
|
35
|
+
const pageData = data.data ?? [];
|
|
36
|
+
// A non-empty page is always real. An empty page may be a transient glitch,
|
|
37
|
+
// so accept it only once retries are spent.
|
|
38
|
+
if (pageData.length > 0 || attempt === MAX_ATTEMPTS) {
|
|
39
|
+
const links = data.links;
|
|
40
|
+
return {
|
|
41
|
+
data: pageData,
|
|
42
|
+
included: data.included ?? [],
|
|
43
|
+
cursor: links?.meta?.nextCursor ?? extractCursor(links?.next),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
lastError = undefined;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
lastError = error;
|
|
50
|
+
}
|
|
51
|
+
if (attempt < MAX_ATTEMPTS)
|
|
52
|
+
await sleep(retryDelayMs);
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Failed to fetch ${path} — ${JSON.stringify(lastError)}`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Repeatedly GET `path`, following `links.next`, until no further page.
|
|
58
|
+
* Returns the concatenated `data` and `included` arrays from every page.
|
|
59
|
+
*/
|
|
60
|
+
async function fetchAllPages(client, path, params, options = {}) {
|
|
61
|
+
const retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS;
|
|
62
|
+
const allData = [];
|
|
63
|
+
const allIncluded = [];
|
|
64
|
+
const seenCursors = new Set();
|
|
65
|
+
let cursor;
|
|
66
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
67
|
+
const query = { ...(params.query ?? {}) };
|
|
68
|
+
if (cursor)
|
|
69
|
+
query['page[cursor]'] = cursor;
|
|
70
|
+
const requestParams = {};
|
|
71
|
+
if (params.path)
|
|
72
|
+
requestParams.path = params.path;
|
|
73
|
+
if (Object.keys(query).length > 0)
|
|
74
|
+
requestParams.query = query;
|
|
75
|
+
const result = await fetchPage(client, path, requestParams, retryDelayMs);
|
|
76
|
+
allData.push(...result.data);
|
|
77
|
+
allIncluded.push(...result.included);
|
|
78
|
+
cursor = result.cursor;
|
|
79
|
+
// Stop on no next page, or if the server hands back a cursor already used.
|
|
80
|
+
if (!cursor || seenCursors.has(cursor))
|
|
81
|
+
break;
|
|
82
|
+
seenCursors.add(cursor);
|
|
83
|
+
}
|
|
84
|
+
return { data: allData, included: allIncluded };
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=pagination.js.map
|
package/dist/playlist.js
CHANGED
|
@@ -19,19 +19,15 @@ exports.moveTrackInPlaylist = moveTrackInPlaylist;
|
|
|
19
19
|
exports.updatePlaylistDescriptionData = updatePlaylistDescriptionData;
|
|
20
20
|
exports.updatePlaylistDescription = updatePlaylistDescription;
|
|
21
21
|
const auth_1 = require("./auth");
|
|
22
|
+
const pagination_1 = require("./pagination");
|
|
22
23
|
async function listPlaylistsData(client, countryCode) {
|
|
23
|
-
const { data
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
countryCode,
|
|
28
|
-
},
|
|
24
|
+
const { data } = await (0, pagination_1.fetchAllPages)(client, '/playlists', {
|
|
25
|
+
query: {
|
|
26
|
+
'filter[owners.id]': ['me'],
|
|
27
|
+
countryCode,
|
|
29
28
|
},
|
|
30
29
|
});
|
|
31
|
-
|
|
32
|
-
throw new Error(`Failed to list playlists — ${JSON.stringify(error)}`);
|
|
33
|
-
}
|
|
34
|
-
return (data.data ?? []).map((p) => ({
|
|
30
|
+
return data.map((p) => ({
|
|
35
31
|
id: p.id,
|
|
36
32
|
name: p.attributes?.name ?? 'Untitled',
|
|
37
33
|
description: p.attributes?.description,
|
|
@@ -187,13 +183,9 @@ async function addTrackToPlaylist(playlistId, trackId, json) {
|
|
|
187
183
|
}
|
|
188
184
|
}
|
|
189
185
|
async function removeTrackFromPlaylistData(playlistId, trackId, client) {
|
|
190
|
-
const { data:
|
|
191
|
-
|
|
186
|
+
const { data: items } = await (0, pagination_1.fetchAllPages)(client, '/playlists/{id}/relationships/items', {
|
|
187
|
+
path: { id: playlistId },
|
|
192
188
|
});
|
|
193
|
-
if (itemsError || !itemsData) {
|
|
194
|
-
throw new Error(`Failed to get playlist items — ${JSON.stringify(itemsError)}`);
|
|
195
|
-
}
|
|
196
|
-
const items = itemsData.data ?? [];
|
|
197
189
|
const item = items.find((i) => i.id === trackId);
|
|
198
190
|
if (!item) {
|
|
199
191
|
throw new Error(`Track ${trackId} not found in playlist ${playlistId}.`);
|
|
@@ -267,13 +259,9 @@ async function addAlbumToPlaylist(playlistId, albumId, json) {
|
|
|
267
259
|
}
|
|
268
260
|
}
|
|
269
261
|
async function moveTrackInPlaylistData(playlistId, trackId, positionBefore, client) {
|
|
270
|
-
const { data:
|
|
271
|
-
|
|
262
|
+
const { data: items } = await (0, pagination_1.fetchAllPages)(client, '/playlists/{id}/relationships/items', {
|
|
263
|
+
path: { id: playlistId },
|
|
272
264
|
});
|
|
273
|
-
if (itemsError || !itemsData) {
|
|
274
|
-
throw new Error(`Failed to get playlist items — ${JSON.stringify(itemsError)}`);
|
|
275
|
-
}
|
|
276
|
-
const items = itemsData.data ?? [];
|
|
277
265
|
const item = items.find((i) => i.id === trackId);
|
|
278
266
|
if (!item) {
|
|
279
267
|
throw new Error(`Track ${trackId} not found in playlist ${playlistId}.`);
|
package/dist/saved.js
CHANGED
|
@@ -7,17 +7,13 @@ exports.addSavedItem = addSavedItem;
|
|
|
7
7
|
exports.removeSavedItemData = removeSavedItemData;
|
|
8
8
|
exports.removeSavedItem = removeSavedItem;
|
|
9
9
|
const auth_1 = require("./auth");
|
|
10
|
+
const pagination_1 = require("./pagination");
|
|
10
11
|
async function listSavedItemsData(client) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
},
|
|
12
|
+
// Paginated: returns the full save-for-later collection, not just the first ~20.
|
|
13
|
+
const { included } = await (0, pagination_1.fetchAllPages)(client, '/userCollectionSaveForLaters/{id}/relationships/items', {
|
|
14
|
+
path: { id: 'me' },
|
|
15
|
+
query: { include: ['items'] },
|
|
16
16
|
});
|
|
17
|
-
if (error || !data) {
|
|
18
|
-
throw new Error(`Failed to list saved items — ${JSON.stringify(error)}`);
|
|
19
|
-
}
|
|
20
|
-
const included = data.included ?? [];
|
|
21
17
|
return included.map((item) => {
|
|
22
18
|
const attrs = item.attributes ?? {};
|
|
23
19
|
return {
|