@lucaperret/tidal-cli 1.1.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/LICENSE +21 -0
- package/README.md +204 -0
- package/dist/album.d.ts +2 -0
- package/dist/album.js +124 -0
- package/dist/artist.d.ts +5 -0
- package/dist/artist.js +230 -0
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js +226 -0
- package/dist/history.d.ts +3 -0
- package/dist/history.js +69 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +372 -0
- package/dist/library.d.ts +7 -0
- package/dist/library.js +131 -0
- package/dist/playback.d.ts +3 -0
- package/dist/playback.js +284 -0
- package/dist/playlist.d.ts +9 -0
- package/dist/playlist.js +269 -0
- package/dist/recommend.d.ts +1 -0
- package/dist/recommend.js +36 -0
- package/dist/search.d.ts +4 -0
- package/dist/search.js +183 -0
- package/dist/session.d.ts +7 -0
- package/dist/session.js +120 -0
- package/dist/track.d.ts +4 -0
- package/dist/track.js +235 -0
- package/dist/user.d.ts +1 -0
- package/dist/user.js +36 -0
- package/package.json +59 -0
package/dist/playlist.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listPlaylists = listPlaylists;
|
|
4
|
+
exports.createPlaylist = createPlaylist;
|
|
5
|
+
exports.renamePlaylist = renamePlaylist;
|
|
6
|
+
exports.deletePlaylist = deletePlaylist;
|
|
7
|
+
exports.addTrackToPlaylist = addTrackToPlaylist;
|
|
8
|
+
exports.removeTrackFromPlaylist = removeTrackFromPlaylist;
|
|
9
|
+
exports.addAlbumToPlaylist = addAlbumToPlaylist;
|
|
10
|
+
exports.moveTrackInPlaylist = moveTrackInPlaylist;
|
|
11
|
+
exports.updatePlaylistDescription = updatePlaylistDescription;
|
|
12
|
+
const auth_1 = require("./auth");
|
|
13
|
+
async function listPlaylists(json) {
|
|
14
|
+
const client = await (0, auth_1.getApiClient)();
|
|
15
|
+
const { data, error } = await client.GET('/playlists', {
|
|
16
|
+
params: {
|
|
17
|
+
query: {
|
|
18
|
+
'filter[owners.id]': ['me'],
|
|
19
|
+
countryCode: await (0, auth_1.getCountryCode)(),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
if (error || !data) {
|
|
24
|
+
console.error(`Error: Failed to list playlists — ${JSON.stringify(error)}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const playlists = (data.data ?? []).map((p) => ({
|
|
28
|
+
id: p.id,
|
|
29
|
+
name: p.attributes?.name ?? 'Untitled',
|
|
30
|
+
description: p.attributes?.description,
|
|
31
|
+
numberOfItems: p.attributes?.numberOfItems,
|
|
32
|
+
createdAt: p.attributes?.createdAt,
|
|
33
|
+
lastModifiedAt: p.attributes?.lastModifiedAt,
|
|
34
|
+
}));
|
|
35
|
+
if (json) {
|
|
36
|
+
console.log(JSON.stringify(playlists, null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (playlists.length === 0) {
|
|
40
|
+
console.log('No playlists found.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
console.log('\nYour playlists:\n');
|
|
44
|
+
for (const p of playlists) {
|
|
45
|
+
console.log(` [${p.id}] ${p.name} (${p.numberOfItems ?? 0} tracks)`);
|
|
46
|
+
if (p.description)
|
|
47
|
+
console.log(` ${p.description}`);
|
|
48
|
+
}
|
|
49
|
+
console.log();
|
|
50
|
+
}
|
|
51
|
+
async function createPlaylist(name, description, json) {
|
|
52
|
+
const client = await (0, auth_1.getApiClient)();
|
|
53
|
+
const { data, error } = await client.POST('/playlists', {
|
|
54
|
+
body: {
|
|
55
|
+
data: {
|
|
56
|
+
type: 'playlists',
|
|
57
|
+
attributes: {
|
|
58
|
+
name,
|
|
59
|
+
description,
|
|
60
|
+
accessType: 'UNLISTED',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (error || !data) {
|
|
66
|
+
console.error(`Error: Failed to create playlist — ${JSON.stringify(error)}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
const created = data.data;
|
|
70
|
+
const result = {
|
|
71
|
+
id: created.id,
|
|
72
|
+
name: created.attributes?.name ?? name,
|
|
73
|
+
description: created.attributes?.description ?? description,
|
|
74
|
+
};
|
|
75
|
+
if (json) {
|
|
76
|
+
console.log(JSON.stringify(result, null, 2));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.log(`\nPlaylist created: [${result.id}] ${result.name}`);
|
|
80
|
+
}
|
|
81
|
+
async function renamePlaylist(playlistId, name, json) {
|
|
82
|
+
const client = await (0, auth_1.getApiClient)();
|
|
83
|
+
const { error } = await client.PATCH('/playlists/{id}', {
|
|
84
|
+
params: { path: { id: playlistId } },
|
|
85
|
+
body: {
|
|
86
|
+
data: {
|
|
87
|
+
type: 'playlists',
|
|
88
|
+
id: playlistId,
|
|
89
|
+
attributes: { name },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
if (error) {
|
|
94
|
+
console.error(`Error: Failed to rename playlist — ${JSON.stringify(error)}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
if (json) {
|
|
98
|
+
console.log(JSON.stringify({ id: playlistId, name, success: true }, null, 2));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
console.log(`\nPlaylist ${playlistId} renamed to "${name}".`);
|
|
102
|
+
}
|
|
103
|
+
async function deletePlaylist(playlistId, json) {
|
|
104
|
+
const client = await (0, auth_1.getApiClient)();
|
|
105
|
+
const { error } = await client.DELETE('/playlists/{id}', {
|
|
106
|
+
params: { path: { id: playlistId } },
|
|
107
|
+
});
|
|
108
|
+
if (error) {
|
|
109
|
+
console.error(`Error: Failed to delete playlist — ${JSON.stringify(error)}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
if (json) {
|
|
113
|
+
console.log(JSON.stringify({ id: playlistId, deleted: true }, null, 2));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log(`\nPlaylist ${playlistId} deleted.`);
|
|
117
|
+
}
|
|
118
|
+
async function addTrackToPlaylist(playlistId, trackId, json) {
|
|
119
|
+
const client = await (0, auth_1.getApiClient)();
|
|
120
|
+
const { error } = await client.POST('/playlists/{id}/relationships/items', {
|
|
121
|
+
params: { path: { id: playlistId } },
|
|
122
|
+
body: {
|
|
123
|
+
data: [{ id: trackId, type: 'tracks' }],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
if (error) {
|
|
127
|
+
console.error(`Error: Failed to add track — ${JSON.stringify(error)}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
if (json) {
|
|
131
|
+
console.log(JSON.stringify({ playlistId, trackId, added: true }, null, 2));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
console.log(`\nTrack ${trackId} added to playlist ${playlistId}.`);
|
|
135
|
+
}
|
|
136
|
+
async function removeTrackFromPlaylist(playlistId, trackId, json) {
|
|
137
|
+
const client = await (0, auth_1.getApiClient)();
|
|
138
|
+
// Get playlist items to find the item index (required by the API)
|
|
139
|
+
const { data: itemsData, error: itemsError } = await client.GET('/playlists/{id}/relationships/items', {
|
|
140
|
+
params: { path: { id: playlistId } },
|
|
141
|
+
});
|
|
142
|
+
if (itemsError || !itemsData) {
|
|
143
|
+
console.error(`Error: Failed to get playlist items — ${JSON.stringify(itemsError)}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const items = itemsData.data ?? [];
|
|
147
|
+
const item = items.find((i) => i.id === trackId);
|
|
148
|
+
if (!item) {
|
|
149
|
+
console.error(`Error: Track ${trackId} not found in playlist ${playlistId}.`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const { error } = await client.DELETE('/playlists/{id}/relationships/items', {
|
|
153
|
+
params: { path: { id: playlistId } },
|
|
154
|
+
body: {
|
|
155
|
+
data: [{ id: trackId, type: 'tracks', meta: { itemId: item.meta.itemId } }],
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
if (error) {
|
|
159
|
+
console.error(`Error: Failed to remove track — ${JSON.stringify(error)}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
if (json) {
|
|
163
|
+
console.log(JSON.stringify({ playlistId, trackId, removed: true }, null, 2));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
console.log(`\nTrack ${trackId} removed from playlist ${playlistId}.`);
|
|
167
|
+
}
|
|
168
|
+
async function addAlbumToPlaylist(playlistId, albumId, json) {
|
|
169
|
+
const client = await (0, auth_1.getApiClient)();
|
|
170
|
+
// First get album tracks
|
|
171
|
+
const { data: albumData, error: albumError } = await client.GET('/albums/{id}', {
|
|
172
|
+
params: {
|
|
173
|
+
path: { id: albumId },
|
|
174
|
+
query: { countryCode: await (0, auth_1.getCountryCode)(), include: ['items'] },
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
if (albumError || !albumData) {
|
|
178
|
+
console.error(`Error: Failed to get album — ${JSON.stringify(albumError)}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
// Extract track IDs from included items
|
|
182
|
+
const included = albumData.included ?? [];
|
|
183
|
+
const trackIds = included
|
|
184
|
+
.filter((item) => item.type === 'tracks')
|
|
185
|
+
.map((item) => ({ id: item.id, type: 'tracks' }));
|
|
186
|
+
if (trackIds.length === 0) {
|
|
187
|
+
console.error('Error: No tracks found in album.');
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const { error } = await client.POST('/playlists/{id}/relationships/items', {
|
|
191
|
+
params: { path: { id: playlistId } },
|
|
192
|
+
body: { data: trackIds },
|
|
193
|
+
});
|
|
194
|
+
if (error) {
|
|
195
|
+
console.error(`Error: Failed to add album tracks — ${JSON.stringify(error)}`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
if (json) {
|
|
199
|
+
console.log(JSON.stringify({ playlistId, albumId, tracksAdded: trackIds.length }, null, 2));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
console.log(`\n${trackIds.length} tracks from album ${albumId} added to playlist ${playlistId}.`);
|
|
203
|
+
}
|
|
204
|
+
async function moveTrackInPlaylist(playlistId, trackId, positionBefore, json) {
|
|
205
|
+
const client = await (0, auth_1.getApiClient)();
|
|
206
|
+
// Get playlist items to find the itemId for the track
|
|
207
|
+
const { data: itemsData, error: itemsError } = await client.GET('/playlists/{id}/relationships/items', {
|
|
208
|
+
params: { path: { id: playlistId } },
|
|
209
|
+
});
|
|
210
|
+
if (itemsError || !itemsData) {
|
|
211
|
+
console.error(`Error: Failed to get playlist items — ${JSON.stringify(itemsError)}`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
const items = itemsData.data ?? [];
|
|
215
|
+
const item = items.find((i) => i.id === trackId);
|
|
216
|
+
if (!item) {
|
|
217
|
+
console.error(`Error: Track ${trackId} not found in playlist ${playlistId}.`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
const itemId = item.meta?.itemId;
|
|
221
|
+
if (!itemId) {
|
|
222
|
+
console.error(`Error: Could not find itemId for track ${trackId}.`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const meta = {};
|
|
226
|
+
if (positionBefore !== 'end') {
|
|
227
|
+
meta.positionBefore = positionBefore;
|
|
228
|
+
}
|
|
229
|
+
const { error } = await client.PATCH('/playlists/{id}/relationships/items', {
|
|
230
|
+
params: { path: { id: playlistId } },
|
|
231
|
+
body: {
|
|
232
|
+
data: [{ id: trackId, type: 'tracks', meta: { itemId } }],
|
|
233
|
+
meta,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
if (error) {
|
|
237
|
+
console.error(`Error: Failed to move track — ${JSON.stringify(error)}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
if (json) {
|
|
241
|
+
console.log(JSON.stringify({ playlistId, trackId, positionBefore, moved: true }, null, 2));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const posDesc = positionBefore === 'end' ? 'to end of playlist' : `before item ${positionBefore}`;
|
|
245
|
+
console.log(`\nTrack ${trackId} moved ${posDesc} in playlist ${playlistId}.`);
|
|
246
|
+
}
|
|
247
|
+
async function updatePlaylistDescription(playlistId, description, json) {
|
|
248
|
+
const client = await (0, auth_1.getApiClient)();
|
|
249
|
+
const { error } = await client.PATCH('/playlists/{id}', {
|
|
250
|
+
params: { path: { id: playlistId } },
|
|
251
|
+
body: {
|
|
252
|
+
data: {
|
|
253
|
+
type: 'playlists',
|
|
254
|
+
id: playlistId,
|
|
255
|
+
attributes: { description },
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
if (error) {
|
|
260
|
+
console.error(`Error: Failed to update playlist description — ${JSON.stringify(error)}`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
if (json) {
|
|
264
|
+
console.log(JSON.stringify({ id: playlistId, description, success: true }, null, 2));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
console.log(`\nPlaylist ${playlistId} description updated.`);
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=playlist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getRecommendations(json: boolean): Promise<void>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRecommendations = getRecommendations;
|
|
4
|
+
const auth_1 = require("./auth");
|
|
5
|
+
async function getRecommendations(json) {
|
|
6
|
+
const client = await (0, auth_1.getApiClient)();
|
|
7
|
+
const { data, error } = await client.GET('/userRecommendations/me', {
|
|
8
|
+
params: {
|
|
9
|
+
query: {
|
|
10
|
+
countryCode: await (0, auth_1.getCountryCode)(),
|
|
11
|
+
include: ['discoveryMixes', 'myMixes', 'newArrivalMixes'],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
if (error || !data) {
|
|
16
|
+
console.error(`Error: Failed to get recommendations — ${JSON.stringify(error)}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const included = data.included ?? [];
|
|
20
|
+
if (json) {
|
|
21
|
+
console.log(JSON.stringify(included, null, 2));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (included.length === 0) {
|
|
25
|
+
console.log('No recommendations found.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log('\nYour recommendations:\n');
|
|
29
|
+
for (const item of included) {
|
|
30
|
+
const attrs = item.attributes;
|
|
31
|
+
const name = attrs?.title ?? attrs?.name ?? 'Untitled';
|
|
32
|
+
console.log(` [${item.id}] (${item.type}) ${name}`);
|
|
33
|
+
}
|
|
34
|
+
console.log();
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=recommend.js.map
|
package/dist/search.d.ts
ADDED
package/dist/search.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.search = search;
|
|
4
|
+
exports.searchSuggestions = searchSuggestions;
|
|
5
|
+
const auth_1 = require("./auth");
|
|
6
|
+
function formatDuration(isoDuration) {
|
|
7
|
+
if (!isoDuration)
|
|
8
|
+
return '';
|
|
9
|
+
// ISO 8601 duration: PT3M45S
|
|
10
|
+
const match = isoDuration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
|
|
11
|
+
if (!match)
|
|
12
|
+
return isoDuration;
|
|
13
|
+
const h = match[1] ? `${match[1]}:` : '';
|
|
14
|
+
const m = (match[2] ?? '0').padStart(h ? 2 : 1, '0');
|
|
15
|
+
const s = (match[3] ?? '0').padStart(2, '0');
|
|
16
|
+
return `${h}${m}:${s}`;
|
|
17
|
+
}
|
|
18
|
+
async function search(type, query, json) {
|
|
19
|
+
const client = await (0, auth_1.getApiClient)();
|
|
20
|
+
const includeMap = {
|
|
21
|
+
artist: 'artists',
|
|
22
|
+
album: 'albums',
|
|
23
|
+
track: 'tracks',
|
|
24
|
+
video: 'videos',
|
|
25
|
+
playlist: 'playlists',
|
|
26
|
+
};
|
|
27
|
+
const { data, error } = await client.GET('/searchResults/{id}', {
|
|
28
|
+
params: {
|
|
29
|
+
path: { id: query },
|
|
30
|
+
query: {
|
|
31
|
+
countryCode: await (0, auth_1.getCountryCode)(),
|
|
32
|
+
include: [includeMap[type]],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
if (error || !data) {
|
|
37
|
+
console.error(`Error: Search failed — ${JSON.stringify(error)}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const included = data.included ?? [];
|
|
41
|
+
const results = [];
|
|
42
|
+
for (const item of included) {
|
|
43
|
+
if (item.type === 'artists' && type === 'artist') {
|
|
44
|
+
const attrs = item.attributes;
|
|
45
|
+
results.push({
|
|
46
|
+
id: item.id,
|
|
47
|
+
type: 'artist',
|
|
48
|
+
name: attrs?.name ?? 'Unknown',
|
|
49
|
+
extra: { popularity: attrs?.popularity },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else if (item.type === 'albums' && type === 'album') {
|
|
53
|
+
const attrs = item.attributes;
|
|
54
|
+
results.push({
|
|
55
|
+
id: item.id,
|
|
56
|
+
type: 'album',
|
|
57
|
+
name: attrs?.title ?? 'Unknown',
|
|
58
|
+
extra: {
|
|
59
|
+
albumType: attrs?.albumType,
|
|
60
|
+
numberOfItems: attrs?.numberOfItems,
|
|
61
|
+
releaseDate: attrs?.releaseDate,
|
|
62
|
+
duration: formatDuration(attrs?.duration),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else if (item.type === 'tracks' && type === 'track') {
|
|
67
|
+
const attrs = item.attributes;
|
|
68
|
+
results.push({
|
|
69
|
+
id: item.id,
|
|
70
|
+
type: 'track',
|
|
71
|
+
name: attrs?.title ?? 'Unknown',
|
|
72
|
+
extra: {
|
|
73
|
+
duration: formatDuration(attrs?.duration),
|
|
74
|
+
explicit: attrs?.explicit,
|
|
75
|
+
isrc: attrs?.isrc,
|
|
76
|
+
popularity: attrs?.popularity,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else if (item.type === 'videos' && type === 'video') {
|
|
81
|
+
const attrs = item.attributes;
|
|
82
|
+
results.push({
|
|
83
|
+
id: item.id,
|
|
84
|
+
type: 'video',
|
|
85
|
+
name: attrs?.title ?? 'Unknown',
|
|
86
|
+
extra: {
|
|
87
|
+
duration: formatDuration(attrs?.duration),
|
|
88
|
+
explicit: attrs?.explicit,
|
|
89
|
+
popularity: attrs?.popularity,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else if (item.type === 'playlists' && type === 'playlist') {
|
|
94
|
+
const attrs = item.attributes;
|
|
95
|
+
results.push({
|
|
96
|
+
id: item.id,
|
|
97
|
+
type: 'playlist',
|
|
98
|
+
name: attrs?.name ?? 'Unknown',
|
|
99
|
+
extra: {
|
|
100
|
+
numberOfItems: attrs?.numberOfItems,
|
|
101
|
+
description: attrs?.description,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Sort by popularity (most relevant first) — skip albums and playlists which lack a direct popularity field
|
|
107
|
+
if (type !== 'album' && type !== 'playlist') {
|
|
108
|
+
results.sort((a, b) => {
|
|
109
|
+
const pa = a.extra?.popularity ?? 0;
|
|
110
|
+
const pb = b.extra?.popularity ?? 0;
|
|
111
|
+
return pb - pa;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (json) {
|
|
115
|
+
console.log(JSON.stringify(results, null, 2));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (results.length === 0) {
|
|
119
|
+
console.log(`No ${type}s found for "${query}".`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const typeLabel = type === 'playlist' ? 'playlists' : `${type}s`;
|
|
123
|
+
console.log(`\nSearch results for "${query}" (${typeLabel}):\n`);
|
|
124
|
+
for (const r of results) {
|
|
125
|
+
const extras = r.extra
|
|
126
|
+
? Object.entries(r.extra)
|
|
127
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
128
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
129
|
+
.join(', ')
|
|
130
|
+
: '';
|
|
131
|
+
console.log(` [${r.id}] ${r.name}${extras ? ` (${extras})` : ''}`);
|
|
132
|
+
}
|
|
133
|
+
console.log();
|
|
134
|
+
}
|
|
135
|
+
async function searchSuggestions(query, json) {
|
|
136
|
+
const client = await (0, auth_1.getApiClient)();
|
|
137
|
+
const { data, error } = await client.GET('/searchSuggestions/{id}', {
|
|
138
|
+
params: {
|
|
139
|
+
path: { id: query },
|
|
140
|
+
query: {
|
|
141
|
+
countryCode: await (0, auth_1.getCountryCode)(),
|
|
142
|
+
include: ['directHits'],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
if (error || !data) {
|
|
147
|
+
console.error(`Error: Failed to get search suggestions — ${JSON.stringify(error)}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
const attrs = data.data?.attributes ?? {};
|
|
151
|
+
const suggestions = (attrs.suggestions ?? []).map((s) => s.query ?? s);
|
|
152
|
+
const included = data.included ?? [];
|
|
153
|
+
const directHits = included.map((item) => {
|
|
154
|
+
const itemAttrs = item.attributes;
|
|
155
|
+
return {
|
|
156
|
+
id: item.id,
|
|
157
|
+
type: item.type,
|
|
158
|
+
name: itemAttrs?.title ?? itemAttrs?.name ?? 'Unknown',
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
const result = { suggestions, directHits };
|
|
162
|
+
if (json) {
|
|
163
|
+
console.log(JSON.stringify(result, null, 2));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (suggestions.length > 0) {
|
|
167
|
+
console.log(`\nSuggestions for "${query}":\n`);
|
|
168
|
+
for (const s of suggestions) {
|
|
169
|
+
console.log(` ${s}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (directHits.length > 0) {
|
|
173
|
+
console.log(`\nDirect hits:\n`);
|
|
174
|
+
for (const h of directHits) {
|
|
175
|
+
console.log(` [${h.id}] (${h.type}) ${h.name}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (suggestions.length === 0 && directHits.length === 0) {
|
|
179
|
+
console.log(`No suggestions found for "${query}".`);
|
|
180
|
+
}
|
|
181
|
+
console.log();
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function loadStorage(): Record<string, string>;
|
|
2
|
+
export declare function saveStorage(data: Record<string, string>): void;
|
|
3
|
+
/**
|
|
4
|
+
* Install a globalThis.localStorage polyfill backed by ~/.tidal-cli/session.json.
|
|
5
|
+
* Must be called before importing @tidal-music/auth.
|
|
6
|
+
*/
|
|
7
|
+
export declare function installLocalStorage(): void;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadStorage = loadStorage;
|
|
37
|
+
exports.saveStorage = saveStorage;
|
|
38
|
+
exports.installLocalStorage = installLocalStorage;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const SESSION_DIR = path.join(os.homedir(), '.tidal-cli');
|
|
43
|
+
const SESSION_FILE = path.join(SESSION_DIR, 'session.json');
|
|
44
|
+
function ensureDir() {
|
|
45
|
+
if (!fs.existsSync(SESSION_DIR)) {
|
|
46
|
+
fs.mkdirSync(SESSION_DIR, { mode: 0o700 });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function loadStorage() {
|
|
50
|
+
ensureDir();
|
|
51
|
+
if (!fs.existsSync(SESSION_FILE))
|
|
52
|
+
return {};
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function saveStorage(data) {
|
|
61
|
+
ensureDir();
|
|
62
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Install a globalThis.localStorage polyfill backed by ~/.tidal-cli/session.json.
|
|
66
|
+
* Must be called before importing @tidal-music/auth.
|
|
67
|
+
*/
|
|
68
|
+
function installLocalStorage() {
|
|
69
|
+
// Always install our file-backed polyfill — Node.js 22+ has a built-in
|
|
70
|
+
// localStorage that requires --localstorage-file to work, so we override it.
|
|
71
|
+
let store = loadStorage();
|
|
72
|
+
const localStorage = {
|
|
73
|
+
getItem(key) {
|
|
74
|
+
// Re-read from disk to pick up any changes
|
|
75
|
+
store = loadStorage();
|
|
76
|
+
return store[key] ?? null;
|
|
77
|
+
},
|
|
78
|
+
setItem(key, value) {
|
|
79
|
+
store[key] = value;
|
|
80
|
+
saveStorage(store);
|
|
81
|
+
},
|
|
82
|
+
removeItem(key) {
|
|
83
|
+
delete store[key];
|
|
84
|
+
saveStorage(store);
|
|
85
|
+
},
|
|
86
|
+
clear() {
|
|
87
|
+
store = {};
|
|
88
|
+
saveStorage(store);
|
|
89
|
+
},
|
|
90
|
+
key(index) {
|
|
91
|
+
return Object.keys(store)[index] ?? null;
|
|
92
|
+
},
|
|
93
|
+
get length() {
|
|
94
|
+
return Object.keys(store).length;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
Object.defineProperty(globalThis, 'localStorage', {
|
|
98
|
+
value: localStorage,
|
|
99
|
+
writable: false,
|
|
100
|
+
configurable: true,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Polyfill browser APIs that @tidal-music/auth expects in Node.js
|
|
104
|
+
if (typeof globalThis.CustomEvent === 'undefined') {
|
|
105
|
+
globalThis.CustomEvent = class CustomEvent extends Event {
|
|
106
|
+
detail;
|
|
107
|
+
constructor(type, params) {
|
|
108
|
+
super(type, params);
|
|
109
|
+
this.detail = params?.detail;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// EventTarget-based dispatchEvent/addEventListener for globalThis
|
|
114
|
+
if (typeof globalThis.dispatchEvent !== 'function') {
|
|
115
|
+
const target = new EventTarget();
|
|
116
|
+
globalThis.addEventListener = target.addEventListener.bind(target);
|
|
117
|
+
globalThis.removeEventListener = target.removeEventListener.bind(target);
|
|
118
|
+
globalThis.dispatchEvent = target.dispatchEvent.bind(target);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=session.js.map
|
package/dist/track.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function getTrackInfo(trackId: string, json: boolean): Promise<void>;
|
|
2
|
+
export declare function getTrackRadio(trackId: string, json: boolean): Promise<void>;
|
|
3
|
+
export declare function getTrackByIsrc(isrc: string, json: boolean): Promise<void>;
|
|
4
|
+
export declare function getSimilarTracks(trackId: string, json: boolean): Promise<void>;
|