@lucaperret/tidal-cli 1.1.0 → 1.2.2
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/README.md +13 -1
- package/dist/album.d.ts +4 -0
- package/dist/album.js +69 -50
- package/dist/artist.d.ts +7 -0
- package/dist/artist.js +139 -91
- package/dist/history.d.ts +3 -2
- package/dist/history.js +27 -17
- package/dist/index.js +96 -1
- package/dist/library.d.ts +25 -2
- package/dist/library.js +97 -48
- package/dist/mix.d.ts +4 -0
- package/dist/mix.js +59 -0
- package/dist/playback.d.ts +4 -0
- package/dist/playback.js +62 -46
- package/dist/playlist.d.ts +43 -0
- package/dist/playlist.js +183 -104
- package/dist/recommend.d.ts +4 -1
- package/dist/recommend.js +71 -19
- package/dist/saved.d.ts +16 -0
- package/dist/saved.js +107 -0
- package/dist/search-history.d.ts +13 -0
- package/dist/search-history.js +94 -0
- package/dist/search.d.ts +4 -2
- package/dist/search.js +66 -45
- package/dist/session.js +17 -7
- package/dist/share.d.ts +5 -0
- package/dist/share.js +53 -0
- package/dist/track.d.ts +14 -0
- package/dist/track.js +128 -91
- package/dist/types.d.ts +158 -0
- package/dist/types.js +4 -0
- package/dist/user.d.ts +3 -0
- package/dist/user.js +27 -18
- package/package.json +12 -6
package/dist/index.js
CHANGED
|
@@ -18,8 +18,22 @@ const artist_1 = require("./artist");
|
|
|
18
18
|
const track_1 = require("./track");
|
|
19
19
|
const album_1 = require("./album");
|
|
20
20
|
const recommend_1 = require("./recommend");
|
|
21
|
+
const mix_1 = require("./mix");
|
|
21
22
|
const user_1 = require("./user");
|
|
22
23
|
const history_1 = require("./history");
|
|
24
|
+
const search_history_1 = require("./search-history");
|
|
25
|
+
const saved_1 = require("./saved");
|
|
26
|
+
const share_1 = require("./share");
|
|
27
|
+
const MIX_CATEGORIES = ['daily', 'discovery', 'new-release', 'offline'];
|
|
28
|
+
function parseMixCategory(value) {
|
|
29
|
+
if (!value)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (!MIX_CATEGORIES.includes(value)) {
|
|
32
|
+
console.error(`Error: invalid mix category "${value}". Use one of: ${MIX_CATEGORIES.join(', ')}`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
23
37
|
const program = new commander_1.Command();
|
|
24
38
|
program
|
|
25
39
|
.name('tidal-cli')
|
|
@@ -85,6 +99,24 @@ searchCmd
|
|
|
85
99
|
.action(wrapAction(async (genre) => {
|
|
86
100
|
await (0, search_1.search)('playlist', genre || 'top hits', getJson());
|
|
87
101
|
}));
|
|
102
|
+
searchCmd
|
|
103
|
+
.command('history')
|
|
104
|
+
.description('List your search history')
|
|
105
|
+
.action(wrapAction(async () => {
|
|
106
|
+
await (0, search_history_1.listSearchHistory)(getJson());
|
|
107
|
+
}));
|
|
108
|
+
searchCmd
|
|
109
|
+
.command('history-delete <entry-id>')
|
|
110
|
+
.description('Delete a single search history entry')
|
|
111
|
+
.action(wrapAction(async (entryId) => {
|
|
112
|
+
await (0, search_history_1.deleteSearchHistoryEntry)(entryId, getJson());
|
|
113
|
+
}));
|
|
114
|
+
searchCmd
|
|
115
|
+
.command('history-clear')
|
|
116
|
+
.description('Clear all search history entries')
|
|
117
|
+
.action(wrapAction(async () => {
|
|
118
|
+
await (0, search_history_1.clearSearchHistory)(getJson());
|
|
119
|
+
}));
|
|
88
120
|
// Artist
|
|
89
121
|
const artistCmd = program
|
|
90
122
|
.command('artist')
|
|
@@ -167,8 +199,71 @@ albumCmd
|
|
|
167
199
|
program
|
|
168
200
|
.command('recommend')
|
|
169
201
|
.description('Get personalized recommendations')
|
|
202
|
+
.option('--type <category>', `Mix category: ${MIX_CATEGORIES.join(', ')}`)
|
|
203
|
+
.action(wrapAction(async (opts) => {
|
|
204
|
+
await (0, recommend_1.getRecommendations)(parseMixCategory(opts.type), getJson());
|
|
205
|
+
}));
|
|
206
|
+
// Mix
|
|
207
|
+
const mixCmd = program
|
|
208
|
+
.command('mix')
|
|
209
|
+
.description('Browse personalized mix contents');
|
|
210
|
+
mixCmd
|
|
211
|
+
.command('items <mix-id>')
|
|
212
|
+
.description('Get items inside a specific mix')
|
|
213
|
+
.requiredOption('--type <category>', `Mix category: ${MIX_CATEGORIES.join(', ')}`)
|
|
214
|
+
.action(wrapAction(async (mixId, opts) => {
|
|
215
|
+
const cat = parseMixCategory(opts.type);
|
|
216
|
+
if (!cat)
|
|
217
|
+
return;
|
|
218
|
+
await (0, mix_1.getMixItems)(cat, mixId, getJson());
|
|
219
|
+
}));
|
|
220
|
+
// Share
|
|
221
|
+
program
|
|
222
|
+
.command('share <type> <id>')
|
|
223
|
+
.description('Create a share link for a track or album (type: track | album)')
|
|
224
|
+
.action(wrapAction(async (type, id) => {
|
|
225
|
+
const normalized = type === 'track' ? 'tracks' : type === 'album' ? 'albums' : null;
|
|
226
|
+
if (!normalized) {
|
|
227
|
+
console.error('Error: type must be "track" or "album"');
|
|
228
|
+
process.exit(2);
|
|
229
|
+
}
|
|
230
|
+
await (0, share_1.createShare)(normalized, id, getJson());
|
|
231
|
+
}));
|
|
232
|
+
// Saved (save for later)
|
|
233
|
+
const savedCmd = program
|
|
234
|
+
.command('saved')
|
|
235
|
+
.description('Manage your save-for-later collection');
|
|
236
|
+
savedCmd
|
|
237
|
+
.command('list')
|
|
238
|
+
.description('List items saved for later')
|
|
170
239
|
.action(wrapAction(async () => {
|
|
171
|
-
await (0,
|
|
240
|
+
await (0, saved_1.listSavedItems)(getJson());
|
|
241
|
+
}));
|
|
242
|
+
savedCmd
|
|
243
|
+
.command('add')
|
|
244
|
+
.description('Save an item for later')
|
|
245
|
+
.requiredOption('--type <type>', 'Item type: tracks, albums, artists, playlists, videos')
|
|
246
|
+
.requiredOption('--id <id>', 'Item ID')
|
|
247
|
+
.action(wrapAction(async (opts) => {
|
|
248
|
+
const allowed = ['tracks', 'albums', 'artists', 'playlists', 'videos'];
|
|
249
|
+
if (!allowed.includes(opts.type)) {
|
|
250
|
+
console.error(`Error: invalid type. Use one of: ${allowed.join(', ')}`);
|
|
251
|
+
process.exit(2);
|
|
252
|
+
}
|
|
253
|
+
await (0, saved_1.addSavedItem)(opts.type, opts.id, getJson());
|
|
254
|
+
}));
|
|
255
|
+
savedCmd
|
|
256
|
+
.command('remove')
|
|
257
|
+
.description('Remove an item from saved')
|
|
258
|
+
.requiredOption('--type <type>', 'Item type')
|
|
259
|
+
.requiredOption('--id <id>', 'Item ID')
|
|
260
|
+
.action(wrapAction(async (opts) => {
|
|
261
|
+
const allowed = ['tracks', 'albums', 'artists', 'playlists', 'videos'];
|
|
262
|
+
if (!allowed.includes(opts.type)) {
|
|
263
|
+
console.error(`Error: invalid type. Use one of: ${allowed.join(', ')}`);
|
|
264
|
+
process.exit(2);
|
|
265
|
+
}
|
|
266
|
+
await (0, saved_1.removeSavedItem)(opts.type, opts.id, getJson());
|
|
172
267
|
}));
|
|
173
268
|
// User
|
|
174
269
|
const userCmd = program
|
package/dist/library.d.ts
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
|
-
type LibraryResourceType
|
|
1
|
+
import type { LibraryResourceType } from './types';
|
|
2
|
+
export type { LibraryResourceType };
|
|
3
|
+
export declare function addToLibraryData(resourceType: LibraryResourceType, resourceId: string, client: any): Promise<{
|
|
4
|
+
resourceType: string;
|
|
5
|
+
resourceId: string;
|
|
6
|
+
added: boolean;
|
|
7
|
+
}>;
|
|
2
8
|
export declare function addToLibrary(resourceType: LibraryResourceType, resourceId: string, json: boolean): Promise<void>;
|
|
9
|
+
export declare function removeFromLibraryData(resourceType: LibraryResourceType, resourceId: string, client: any): Promise<{
|
|
10
|
+
resourceType: string;
|
|
11
|
+
resourceId: string;
|
|
12
|
+
removed: boolean;
|
|
13
|
+
}>;
|
|
3
14
|
export declare function removeFromLibrary(resourceType: LibraryResourceType, resourceId: string, json: boolean): Promise<void>;
|
|
15
|
+
export declare function listFavoritedPlaylistsData(client: any): Promise<Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
numberOfItems?: number;
|
|
19
|
+
}>>;
|
|
4
20
|
export declare function listFavoritedPlaylists(json: boolean): Promise<void>;
|
|
21
|
+
export declare function addPlaylistToFavoritesData(playlistId: string, client: any): Promise<{
|
|
22
|
+
playlistId: string;
|
|
23
|
+
added: boolean;
|
|
24
|
+
}>;
|
|
5
25
|
export declare function addPlaylistToFavorites(playlistId: string, json: boolean): Promise<void>;
|
|
26
|
+
export declare function removePlaylistFromFavoritesData(playlistId: string, client: any): Promise<{
|
|
27
|
+
playlistId: string;
|
|
28
|
+
removed: boolean;
|
|
29
|
+
}>;
|
|
6
30
|
export declare function removePlaylistFromFavorites(playlistId: string, json: boolean): Promise<void>;
|
|
7
|
-
export {};
|
package/dist/library.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addToLibraryData = addToLibraryData;
|
|
3
4
|
exports.addToLibrary = addToLibrary;
|
|
5
|
+
exports.removeFromLibraryData = removeFromLibraryData;
|
|
4
6
|
exports.removeFromLibrary = removeFromLibrary;
|
|
7
|
+
exports.listFavoritedPlaylistsData = listFavoritedPlaylistsData;
|
|
5
8
|
exports.listFavoritedPlaylists = listFavoritedPlaylists;
|
|
9
|
+
exports.addPlaylistToFavoritesData = addPlaylistToFavoritesData;
|
|
6
10
|
exports.addPlaylistToFavorites = addPlaylistToFavorites;
|
|
11
|
+
exports.removePlaylistFromFavoritesData = removePlaylistFromFavoritesData;
|
|
7
12
|
exports.removePlaylistFromFavorites = removePlaylistFromFavorites;
|
|
8
13
|
const auth_1 = require("./auth");
|
|
9
14
|
const collectionEndpoints = {
|
|
@@ -12,8 +17,7 @@ const collectionEndpoints = {
|
|
|
12
17
|
track: { path: '/userCollectionTracks/{id}/relationships/items', type: 'tracks' },
|
|
13
18
|
video: { path: '/userCollectionVideos/{id}/relationships/items', type: 'videos' },
|
|
14
19
|
};
|
|
15
|
-
async function
|
|
16
|
-
const client = await (0, auth_1.getApiClient)();
|
|
20
|
+
async function addToLibraryData(resourceType, resourceId, client) {
|
|
17
21
|
const endpoint = collectionEndpoints[resourceType];
|
|
18
22
|
const { error } = await client.POST(endpoint.path, {
|
|
19
23
|
params: { path: { id: 'me' } },
|
|
@@ -22,17 +26,26 @@ async function addToLibrary(resourceType, resourceId, json) {
|
|
|
22
26
|
},
|
|
23
27
|
});
|
|
24
28
|
if (error) {
|
|
25
|
-
|
|
26
|
-
process.exit(1);
|
|
29
|
+
throw new Error(`Failed to add ${resourceType} to library — ${JSON.stringify(error)}`);
|
|
27
30
|
}
|
|
28
|
-
|
|
29
|
-
console.log(JSON.stringify({ resourceType, resourceId, added: true }, null, 2));
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
console.log(`\n${capitalize(resourceType)} ${resourceId} added to your library.`);
|
|
31
|
+
return { resourceType, resourceId, added: true };
|
|
33
32
|
}
|
|
34
|
-
async function
|
|
33
|
+
async function addToLibrary(resourceType, resourceId, json) {
|
|
35
34
|
const client = await (0, auth_1.getApiClient)();
|
|
35
|
+
try {
|
|
36
|
+
const result = await addToLibraryData(resourceType, resourceId, client);
|
|
37
|
+
if (json) {
|
|
38
|
+
console.log(JSON.stringify(result, null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.log(`\n${capitalize(resourceType)} ${resourceId} added to your library.`);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error(`Error: ${err.message}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function removeFromLibraryData(resourceType, resourceId, client) {
|
|
36
49
|
const endpoint = collectionEndpoints[resourceType];
|
|
37
50
|
const { error } = await client.DELETE(endpoint.path, {
|
|
38
51
|
params: { path: { id: 'me' } },
|
|
@@ -41,17 +54,26 @@ async function removeFromLibrary(resourceType, resourceId, json) {
|
|
|
41
54
|
},
|
|
42
55
|
});
|
|
43
56
|
if (error) {
|
|
44
|
-
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
if (json) {
|
|
48
|
-
console.log(JSON.stringify({ resourceType, resourceId, removed: true }, null, 2));
|
|
49
|
-
return;
|
|
57
|
+
throw new Error(`Failed to remove ${resourceType} from library — ${JSON.stringify(error)}`);
|
|
50
58
|
}
|
|
51
|
-
|
|
59
|
+
return { resourceType, resourceId, removed: true };
|
|
52
60
|
}
|
|
53
|
-
async function
|
|
61
|
+
async function removeFromLibrary(resourceType, resourceId, json) {
|
|
54
62
|
const client = await (0, auth_1.getApiClient)();
|
|
63
|
+
try {
|
|
64
|
+
const result = await removeFromLibraryData(resourceType, resourceId, client);
|
|
65
|
+
if (json) {
|
|
66
|
+
console.log(JSON.stringify(result, null, 2));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
console.log(`\n${capitalize(resourceType)} ${resourceId} removed from your library.`);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error(`Error: ${err.message}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function listFavoritedPlaylistsData(client) {
|
|
55
77
|
const { data, error } = await client.GET('/userCollectionPlaylists/{id}/relationships/items', {
|
|
56
78
|
params: {
|
|
57
79
|
path: { id: 'me' },
|
|
@@ -61,11 +83,10 @@ async function listFavoritedPlaylists(json) {
|
|
|
61
83
|
},
|
|
62
84
|
});
|
|
63
85
|
if (error || !data) {
|
|
64
|
-
|
|
65
|
-
process.exit(1);
|
|
86
|
+
throw new Error(`Failed to list favorited playlists — ${JSON.stringify(error)}`);
|
|
66
87
|
}
|
|
67
88
|
const included = data.included ?? [];
|
|
68
|
-
|
|
89
|
+
return included
|
|
69
90
|
.filter((item) => item.type === 'playlists')
|
|
70
91
|
.map((item) => {
|
|
71
92
|
const attrs = item.attributes;
|
|
@@ -75,22 +96,31 @@ async function listFavoritedPlaylists(json) {
|
|
|
75
96
|
numberOfItems: attrs?.numberOfItems,
|
|
76
97
|
};
|
|
77
98
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
}
|
|
100
|
+
async function listFavoritedPlaylists(json) {
|
|
101
|
+
const client = await (0, auth_1.getApiClient)();
|
|
102
|
+
try {
|
|
103
|
+
const playlists = await listFavoritedPlaylistsData(client);
|
|
104
|
+
if (json) {
|
|
105
|
+
console.log(JSON.stringify(playlists, null, 2));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (playlists.length === 0) {
|
|
109
|
+
console.log('No favorited playlists found.');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log('\nFavorited playlists:\n');
|
|
113
|
+
for (const p of playlists) {
|
|
114
|
+
console.log(` [${p.id}] ${p.name} (${p.numberOfItems ?? 0} items)`);
|
|
115
|
+
}
|
|
116
|
+
console.log();
|
|
85
117
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
118
|
+
catch (err) {
|
|
119
|
+
console.error(`Error: ${err.message}`);
|
|
120
|
+
process.exit(1);
|
|
89
121
|
}
|
|
90
|
-
console.log();
|
|
91
122
|
}
|
|
92
|
-
async function
|
|
93
|
-
const client = await (0, auth_1.getApiClient)();
|
|
123
|
+
async function addPlaylistToFavoritesData(playlistId, client) {
|
|
94
124
|
const { error } = await client.POST('/userCollectionPlaylists/{id}/relationships/items', {
|
|
95
125
|
params: { path: { id: 'me' } },
|
|
96
126
|
body: {
|
|
@@ -98,17 +128,26 @@ async function addPlaylistToFavorites(playlistId, json) {
|
|
|
98
128
|
},
|
|
99
129
|
});
|
|
100
130
|
if (error) {
|
|
101
|
-
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
if (json) {
|
|
105
|
-
console.log(JSON.stringify({ playlistId, added: true }, null, 2));
|
|
106
|
-
return;
|
|
131
|
+
throw new Error(`Failed to add playlist to favorites — ${JSON.stringify(error)}`);
|
|
107
132
|
}
|
|
108
|
-
|
|
133
|
+
return { playlistId, added: true };
|
|
109
134
|
}
|
|
110
|
-
async function
|
|
135
|
+
async function addPlaylistToFavorites(playlistId, json) {
|
|
111
136
|
const client = await (0, auth_1.getApiClient)();
|
|
137
|
+
try {
|
|
138
|
+
const result = await addPlaylistToFavoritesData(playlistId, client);
|
|
139
|
+
if (json) {
|
|
140
|
+
console.log(JSON.stringify(result, null, 2));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
console.log(`\nPlaylist ${playlistId} added to favorites.`);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
console.error(`Error: ${err.message}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function removePlaylistFromFavoritesData(playlistId, client) {
|
|
112
151
|
const { error } = await client.DELETE('/userCollectionPlaylists/{id}/relationships/items', {
|
|
113
152
|
params: { path: { id: 'me' } },
|
|
114
153
|
body: {
|
|
@@ -116,14 +155,24 @@ async function removePlaylistFromFavorites(playlistId, json) {
|
|
|
116
155
|
},
|
|
117
156
|
});
|
|
118
157
|
if (error) {
|
|
119
|
-
|
|
120
|
-
process.exit(1);
|
|
158
|
+
throw new Error(`Failed to remove playlist from favorites — ${JSON.stringify(error)}`);
|
|
121
159
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
160
|
+
return { playlistId, removed: true };
|
|
161
|
+
}
|
|
162
|
+
async function removePlaylistFromFavorites(playlistId, json) {
|
|
163
|
+
const client = await (0, auth_1.getApiClient)();
|
|
164
|
+
try {
|
|
165
|
+
const result = await removePlaylistFromFavoritesData(playlistId, client);
|
|
166
|
+
if (json) {
|
|
167
|
+
console.log(JSON.stringify(result, null, 2));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
console.log(`\nPlaylist ${playlistId} removed from favorites.`);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
console.error(`Error: ${err.message}`);
|
|
174
|
+
process.exit(1);
|
|
125
175
|
}
|
|
126
|
-
console.log(`\nPlaylist ${playlistId} removed from favorites.`);
|
|
127
176
|
}
|
|
128
177
|
function capitalize(s) {
|
|
129
178
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
package/dist/mix.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MixCategory, MixItem } from './types';
|
|
2
|
+
export type { MixItem };
|
|
3
|
+
export declare function getMixItemsData(category: MixCategory, mixId: string, client: any, countryCode: string): Promise<MixItem[]>;
|
|
4
|
+
export declare function getMixItems(category: MixCategory, mixId: string, json: boolean): Promise<void>;
|
package/dist/mix.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getMixItemsData = getMixItemsData;
|
|
4
|
+
exports.getMixItems = getMixItems;
|
|
5
|
+
const auth_1 = require("./auth");
|
|
6
|
+
const categoryEndpoints = {
|
|
7
|
+
daily: '/userDailyMixes/{id}/relationships/items',
|
|
8
|
+
discovery: '/userDiscoveryMixes/{id}/relationships/items',
|
|
9
|
+
'new-release': '/userNewReleaseMixes/{id}/relationships/items',
|
|
10
|
+
offline: '/userOfflineMixes/{id}/relationships/items',
|
|
11
|
+
};
|
|
12
|
+
async function getMixItemsData(category, mixId, client, countryCode) {
|
|
13
|
+
const { data, error } = await client.GET(categoryEndpoints[category], {
|
|
14
|
+
params: {
|
|
15
|
+
path: { id: mixId },
|
|
16
|
+
query: {
|
|
17
|
+
countryCode,
|
|
18
|
+
include: ['items'],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (error || !data) {
|
|
23
|
+
throw new Error(`Failed to get mix items — ${JSON.stringify(error)}`);
|
|
24
|
+
}
|
|
25
|
+
const included = data.included ?? [];
|
|
26
|
+
return included.map((item) => {
|
|
27
|
+
const attrs = item.attributes ?? {};
|
|
28
|
+
return {
|
|
29
|
+
id: item.id,
|
|
30
|
+
type: item.type,
|
|
31
|
+
name: attrs.title ?? attrs.name ?? 'Untitled',
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function getMixItems(category, mixId, json) {
|
|
36
|
+
const client = await (0, auth_1.getApiClient)();
|
|
37
|
+
const countryCode = await (0, auth_1.getCountryCode)();
|
|
38
|
+
try {
|
|
39
|
+
const items = await getMixItemsData(category, mixId, client, countryCode);
|
|
40
|
+
if (json) {
|
|
41
|
+
console.log(JSON.stringify(items, null, 2));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (items.length === 0) {
|
|
45
|
+
console.log('No items found in this mix.');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.log(`\n${category} mix [${mixId}] items:\n`);
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
console.log(` [${item.id}] (${item.type}) ${item.name}`);
|
|
51
|
+
}
|
|
52
|
+
console.log();
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.error(`Error: ${err.message}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=mix.js.map
|
package/dist/playback.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { PlaybackInfo, PlaybackUrl } from './types';
|
|
2
|
+
export type { PlaybackInfo, PlaybackUrl };
|
|
3
|
+
export declare function playbackInfoData(trackId: string, quality: string, client: any): Promise<PlaybackInfo>;
|
|
1
4
|
export declare function playbackInfo(trackId: string, quality: string, json: boolean): Promise<void>;
|
|
5
|
+
export declare function playbackUrlData(trackId: string, quality: string, client: any): Promise<PlaybackUrl>;
|
|
2
6
|
export declare function playbackUrl(trackId: string, quality: string, json: boolean): Promise<void>;
|
|
3
7
|
export declare function playbackPlay(trackId: string, quality: string): Promise<void>;
|
package/dist/playback.js
CHANGED
|
@@ -33,7 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.playbackInfoData = playbackInfoData;
|
|
36
37
|
exports.playbackInfo = playbackInfo;
|
|
38
|
+
exports.playbackUrlData = playbackUrlData;
|
|
37
39
|
exports.playbackUrl = playbackUrl;
|
|
38
40
|
exports.playbackPlay = playbackPlay;
|
|
39
41
|
const auth_1 = require("./auth");
|
|
@@ -62,8 +64,7 @@ function parseDataUri(dataUri) {
|
|
|
62
64
|
throw new Error('Invalid data URI from trackManifests');
|
|
63
65
|
return { mimeType: match[1], data: match[2] };
|
|
64
66
|
}
|
|
65
|
-
async function
|
|
66
|
-
const client = await (0, auth_1.getApiClient)();
|
|
67
|
+
async function fetchTrackManifestData(trackId, quality, client) {
|
|
67
68
|
const formats = qualityToFormats[quality] ?? qualityToFormats.HIGH;
|
|
68
69
|
const { data, error } = await client.GET('/trackManifests/{id}', {
|
|
69
70
|
params: {
|
|
@@ -78,13 +79,11 @@ async function fetchTrackManifest(trackId, quality) {
|
|
|
78
79
|
},
|
|
79
80
|
});
|
|
80
81
|
if (error || !data) {
|
|
81
|
-
|
|
82
|
-
process.exit(1);
|
|
82
|
+
throw new Error(`Failed to get track manifest — ${JSON.stringify(error)}`);
|
|
83
83
|
}
|
|
84
84
|
const attrs = data.data?.attributes;
|
|
85
85
|
if (!attrs?.uri) {
|
|
86
|
-
|
|
87
|
-
process.exit(1);
|
|
86
|
+
throw new Error('No manifest URI in response.');
|
|
88
87
|
}
|
|
89
88
|
const { mimeType, data: manifestBase64 } = parseDataUri(attrs.uri);
|
|
90
89
|
return {
|
|
@@ -101,6 +100,11 @@ async function fetchTrackManifest(trackId, quality) {
|
|
|
101
100
|
albumPeakAmplitude: attrs.albumAudioNormalizationData?.peakAmplitude ?? 0,
|
|
102
101
|
};
|
|
103
102
|
}
|
|
103
|
+
// Keep backward-compatible internal helper
|
|
104
|
+
async function fetchTrackManifest(trackId, quality) {
|
|
105
|
+
const client = await (0, auth_1.getApiClient)();
|
|
106
|
+
return fetchTrackManifestData(trackId, quality, client);
|
|
107
|
+
}
|
|
104
108
|
function decodeManifest(base64Manifest, mimeType) {
|
|
105
109
|
const decoded = Buffer.from(base64Manifest, 'base64').toString('utf-8');
|
|
106
110
|
// JSON manifest (BTS): has urls array
|
|
@@ -118,7 +122,6 @@ function decodeManifest(base64Manifest, mimeType) {
|
|
|
118
122
|
const initMatch = decoded.match(/initialization="([^"]+)"/);
|
|
119
123
|
const mediaMatch = decoded.match(/media="([^"]+)"/);
|
|
120
124
|
const codecsMatch = decoded.match(/codecs="([^"]+)"/);
|
|
121
|
-
// Parse segment timeline: <S d="176128" r="6"/> means 7 segments, <S d="89088"/> means 1
|
|
122
125
|
const segmentDurations = [];
|
|
123
126
|
const sMatches = decoded.matchAll(/<S d="(\d+)"(?:\s+r="(\d+)")?\/>/g);
|
|
124
127
|
let segNum = 1;
|
|
@@ -139,7 +142,6 @@ function decodeManifest(base64Manifest, mimeType) {
|
|
|
139
142
|
codecs: codecsMatch?.[1],
|
|
140
143
|
};
|
|
141
144
|
}
|
|
142
|
-
// Fallback: try BaseURL
|
|
143
145
|
const baseUrlMatch = decoded.match(/<BaseURL>([^<]+)<\/BaseURL>/);
|
|
144
146
|
if (baseUrlMatch) {
|
|
145
147
|
return { type: 'direct', url: baseUrlMatch[1], codecs: codecsMatch?.[1] };
|
|
@@ -148,12 +150,10 @@ function decodeManifest(base64Manifest, mimeType) {
|
|
|
148
150
|
throw new Error('Unable to parse manifest');
|
|
149
151
|
}
|
|
150
152
|
async function downloadDashStream(dash) {
|
|
151
|
-
// Download init segment
|
|
152
153
|
const initRes = await fetch(dash.initUrl);
|
|
153
154
|
if (!initRes.ok)
|
|
154
155
|
throw new Error(`Failed to download init segment (${initRes.status})`);
|
|
155
156
|
const initBuf = Buffer.from(await initRes.arrayBuffer());
|
|
156
|
-
// Download media segments
|
|
157
157
|
const segBuffers = [initBuf];
|
|
158
158
|
for (const segNum of dash.segments) {
|
|
159
159
|
const segUrl = dash.mediaTemplate.replace('$Number$', String(segNum));
|
|
@@ -164,9 +164,9 @@ async function downloadDashStream(dash) {
|
|
|
164
164
|
}
|
|
165
165
|
return Buffer.concat(segBuffers);
|
|
166
166
|
}
|
|
167
|
-
async function
|
|
168
|
-
const info = await
|
|
169
|
-
|
|
167
|
+
async function playbackInfoData(trackId, quality, client) {
|
|
168
|
+
const info = await fetchTrackManifestData(trackId, quality, client);
|
|
169
|
+
return {
|
|
170
170
|
trackId: info.trackId,
|
|
171
171
|
presentation: info.trackPresentation,
|
|
172
172
|
previewReason: info.previewReason,
|
|
@@ -178,57 +178,73 @@ async function playbackInfo(trackId, quality, json) {
|
|
|
178
178
|
albumReplayGain: info.albumReplayGain,
|
|
179
179
|
albumPeakAmplitude: info.albumPeakAmplitude,
|
|
180
180
|
};
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
}
|
|
182
|
+
async function playbackInfo(trackId, quality, json) {
|
|
183
|
+
const client = await (0, auth_1.getApiClient)();
|
|
184
|
+
try {
|
|
185
|
+
const result = await playbackInfoData(trackId, quality, client);
|
|
186
|
+
if (json) {
|
|
187
|
+
console.log(JSON.stringify(result, null, 2));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
console.log(`\nPlayback info for track ${trackId}:\n`);
|
|
191
|
+
console.log(` Quality: ${result.audioQuality}`);
|
|
192
|
+
console.log(` Formats: ${result.formats?.join(', ')}`);
|
|
193
|
+
console.log(` Presentation: ${result.presentation}`);
|
|
194
|
+
if (result.previewReason) {
|
|
195
|
+
console.log(` Preview reason: ${result.previewReason}`);
|
|
196
|
+
}
|
|
197
|
+
console.log(` Manifest type: ${result.manifestMimeType}`);
|
|
198
|
+
console.log(` Track gain: ${result.trackReplayGain} dB`);
|
|
199
|
+
console.log(` Album gain: ${result.albumReplayGain} dB`);
|
|
200
|
+
console.log();
|
|
184
201
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
console.log(` Presentation: ${result.presentation}`);
|
|
189
|
-
if (result.previewReason) {
|
|
190
|
-
console.log(` Preview reason: ${result.previewReason}`);
|
|
202
|
+
catch (err) {
|
|
203
|
+
console.error(`Error: ${err.message}`);
|
|
204
|
+
process.exit(1);
|
|
191
205
|
}
|
|
192
|
-
console.log(` Manifest type: ${result.manifestMimeType}`);
|
|
193
|
-
console.log(` Track gain: ${result.trackReplayGain} dB`);
|
|
194
|
-
console.log(` Album gain: ${result.albumReplayGain} dB`);
|
|
195
|
-
console.log();
|
|
196
206
|
}
|
|
197
|
-
async function
|
|
198
|
-
const info = await
|
|
207
|
+
async function playbackUrlData(trackId, quality, client) {
|
|
208
|
+
const info = await fetchTrackManifestData(trackId, quality, client);
|
|
199
209
|
const stream = decodeManifest(info.manifest, info.manifestMimeType);
|
|
200
210
|
if (stream.type === 'direct') {
|
|
201
|
-
|
|
202
|
-
console.log(JSON.stringify({ trackId: info.trackId, url: stream.url, audioQuality: info.audioQuality }, null, 2));
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
console.log(stream.url);
|
|
206
|
-
}
|
|
211
|
+
return { trackId: info.trackId, url: stream.url, audioQuality: info.audioQuality, type: 'direct' };
|
|
207
212
|
}
|
|
208
213
|
else if (stream.dash) {
|
|
214
|
+
return {
|
|
215
|
+
trackId: info.trackId,
|
|
216
|
+
type: 'dash',
|
|
217
|
+
initUrl: stream.dash.initUrl,
|
|
218
|
+
segmentCount: stream.dash.segments.length,
|
|
219
|
+
audioQuality: info.audioQuality,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
throw new Error('No stream URL available for this track.');
|
|
223
|
+
}
|
|
224
|
+
async function playbackUrl(trackId, quality, json) {
|
|
225
|
+
const client = await (0, auth_1.getApiClient)();
|
|
226
|
+
try {
|
|
227
|
+
const result = await playbackUrlData(trackId, quality, client);
|
|
209
228
|
if (json) {
|
|
210
|
-
console.log(JSON.stringify(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
audioQuality: info.audioQuality,
|
|
216
|
-
}, null, 2));
|
|
229
|
+
console.log(JSON.stringify(result, null, 2));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (result.type === 'direct') {
|
|
233
|
+
console.log(result.url);
|
|
217
234
|
}
|
|
218
235
|
else {
|
|
219
|
-
console.log(`DASH stream (${
|
|
220
|
-
console.log(` Init: ${
|
|
236
|
+
console.log(`DASH stream (${result.segmentCount} segments)`);
|
|
237
|
+
console.log(` Init: ${result.initUrl}`);
|
|
221
238
|
}
|
|
222
239
|
}
|
|
223
|
-
|
|
224
|
-
console.error(
|
|
240
|
+
catch (err) {
|
|
241
|
+
console.error(`Error: ${err.message}`);
|
|
225
242
|
process.exit(1);
|
|
226
243
|
}
|
|
227
244
|
}
|
|
228
245
|
async function playbackPlay(trackId, quality) {
|
|
229
246
|
const info = await fetchTrackManifest(trackId, quality);
|
|
230
247
|
const stream = decodeManifest(info.manifest, info.manifestMimeType);
|
|
231
|
-
// Determine file extension from format
|
|
232
248
|
const isFlac = info.formats.includes('FLAC') || info.formats.includes('FLAC_HIRES');
|
|
233
249
|
const ext = isFlac ? '.flac' : '.mp4';
|
|
234
250
|
const tmpFile = path.join(os.tmpdir(), `tidal-${trackId}${ext}`);
|