@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/dist/track.js CHANGED
@@ -1,8 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTrackInfoData = getTrackInfoData;
3
4
  exports.getTrackInfo = getTrackInfo;
5
+ exports.getTrackRadioData = getTrackRadioData;
4
6
  exports.getTrackRadio = getTrackRadio;
7
+ exports.getTrackByIsrcData = getTrackByIsrcData;
5
8
  exports.getTrackByIsrc = getTrackByIsrc;
9
+ exports.getSimilarTracksData = getSimilarTracksData;
6
10
  exports.getSimilarTracks = getSimilarTracks;
7
11
  const auth_1 = require("./auth");
8
12
  function formatDuration(isoDuration) {
@@ -16,20 +20,18 @@ function formatDuration(isoDuration) {
16
20
  const s = (match[3] ?? '0').padStart(2, '0');
17
21
  return `${h}${m}:${s}`;
18
22
  }
19
- async function getTrackInfo(trackId, json) {
20
- const client = await (0, auth_1.getApiClient)();
23
+ async function getTrackInfoData(trackId, client, countryCode) {
21
24
  const { data, error } = await client.GET('/tracks/{id}', {
22
25
  params: {
23
26
  path: { id: trackId },
24
27
  query: {
25
- countryCode: await (0, auth_1.getCountryCode)(),
28
+ countryCode,
26
29
  include: ['artists', 'albums'],
27
30
  },
28
31
  },
29
32
  });
30
33
  if (error || !data) {
31
- console.error(`Error: Failed to get track info — ${JSON.stringify(error)}`);
32
- process.exit(1);
34
+ throw new Error(`Failed to get track info — ${JSON.stringify(error)}`);
33
35
  }
34
36
  const attrs = data.data?.attributes ?? {};
35
37
  const included = data.included ?? [];
@@ -38,25 +40,23 @@ async function getTrackInfo(trackId, json) {
38
40
  .map((item) => item.attributes?.name ?? item.id);
39
41
  const album = included.find((item) => item.type === 'albums');
40
42
  const albumName = album?.attributes?.title ?? undefined;
41
- // Fetch cover art if we have an album
42
43
  let coverUrl;
43
44
  if (album?.id) {
44
45
  try {
45
46
  const { data: artData } = await client.GET('/albums/{id}/relationships/coverArt', {
46
47
  params: {
47
48
  path: { id: album.id },
48
- query: { countryCode: await (0, auth_1.getCountryCode)(), include: ['coverArt'] },
49
+ query: { countryCode, include: ['coverArt'] },
49
50
  },
50
51
  });
51
52
  const artwork = (artData?.included ?? []).find((i) => i.type === 'artworks');
52
53
  const files = artwork?.attributes?.files ?? [];
53
- // Pick 640x640 or the largest available
54
54
  const preferred = files.find((f) => f.meta?.width === 640) ?? files[0];
55
55
  coverUrl = preferred?.href;
56
56
  }
57
57
  catch { }
58
58
  }
59
- const result = {
59
+ return {
60
60
  id: trackId,
61
61
  title: attrs.title ?? 'Unknown',
62
62
  artists,
@@ -69,50 +69,58 @@ async function getTrackInfo(trackId, json) {
69
69
  explicit: attrs.explicit,
70
70
  coverUrl,
71
71
  };
72
- if (json) {
73
- console.log(JSON.stringify(result, null, 2));
74
- return;
75
- }
76
- console.log(`\nTrack: [${result.id}] ${result.title}`);
77
- if (result.artists.length > 0)
78
- console.log(` Artists: ${result.artists.join(', ')}`);
79
- if (result.album)
80
- console.log(` Album: ${result.album}`);
81
- if (result.duration)
82
- console.log(` Duration: ${result.duration}`);
83
- if (result.isrc)
84
- console.log(` ISRC: ${result.isrc}`);
85
- if (result.bpm !== undefined)
86
- console.log(` BPM: ${result.bpm}`);
87
- if (result.key !== undefined)
88
- console.log(` Key: ${result.key}`);
89
- if (result.popularity !== undefined)
90
- console.log(` Popularity: ${result.popularity}`);
91
- if (result.explicit !== undefined)
92
- console.log(` Explicit: ${result.explicit}`);
93
- if (result.coverUrl)
94
- console.log(` Cover: ${result.coverUrl}`);
95
- console.log();
96
72
  }
97
- async function getTrackRadio(trackId, json) {
73
+ async function getTrackInfo(trackId, json) {
98
74
  const client = await (0, auth_1.getApiClient)();
75
+ const countryCode = await (0, auth_1.getCountryCode)();
76
+ try {
77
+ const result = await getTrackInfoData(trackId, client, countryCode);
78
+ if (json) {
79
+ console.log(JSON.stringify(result, null, 2));
80
+ return;
81
+ }
82
+ console.log(`\nTrack: [${result.id}] ${result.title}`);
83
+ if (result.artists.length > 0)
84
+ console.log(` Artists: ${result.artists.join(', ')}`);
85
+ if (result.album)
86
+ console.log(` Album: ${result.album}`);
87
+ if (result.duration)
88
+ console.log(` Duration: ${result.duration}`);
89
+ if (result.isrc)
90
+ console.log(` ISRC: ${result.isrc}`);
91
+ if (result.bpm !== undefined)
92
+ console.log(` BPM: ${result.bpm}`);
93
+ if (result.key !== undefined)
94
+ console.log(` Key: ${result.key}`);
95
+ if (result.popularity !== undefined)
96
+ console.log(` Popularity: ${result.popularity}`);
97
+ if (result.explicit !== undefined)
98
+ console.log(` Explicit: ${result.explicit}`);
99
+ if (result.coverUrl)
100
+ console.log(` Cover: ${result.coverUrl}`);
101
+ console.log();
102
+ }
103
+ catch (err) {
104
+ console.error(`Error: ${err.message}`);
105
+ process.exit(1);
106
+ }
107
+ }
108
+ async function getTrackRadioData(trackId, client, countryCode) {
99
109
  const { data, error } = await client.GET('/tracks/{id}/relationships/radio', {
100
110
  params: {
101
111
  path: { id: trackId },
102
112
  query: {
103
- countryCode: await (0, auth_1.getCountryCode)(),
113
+ countryCode,
104
114
  include: ['radio'],
105
115
  },
106
116
  },
107
117
  });
108
118
  if (error || !data) {
109
- console.error(`Error: Failed to get track radio — ${JSON.stringify(error)}`);
110
- process.exit(1);
119
+ throw new Error(`Failed to get track radio — ${JSON.stringify(error)}`);
111
120
  }
112
- // Radio returns playlists (mix playlists), not individual tracks
113
121
  const radioData = data.data ?? [];
114
122
  const included = data.included ?? [];
115
- const playlists = radioData.map((item) => {
123
+ return radioData.map((item) => {
116
124
  const incl = included.find((i) => i.id === item.id && i.type === 'playlists');
117
125
  const attrs = incl?.attributes ?? {};
118
126
  return {
@@ -122,38 +130,47 @@ async function getTrackRadio(trackId, json) {
122
130
  numberOfItems: attrs.numberOfItems,
123
131
  };
124
132
  });
125
- if (json) {
126
- console.log(JSON.stringify(playlists, null, 2));
127
- return;
128
- }
129
- if (playlists.length === 0) {
130
- console.log(`No radio found for track ${trackId}.`);
131
- return;
133
+ }
134
+ async function getTrackRadio(trackId, json) {
135
+ const client = await (0, auth_1.getApiClient)();
136
+ const countryCode = await (0, auth_1.getCountryCode)();
137
+ try {
138
+ const playlists = await getTrackRadioData(trackId, client, countryCode);
139
+ if (json) {
140
+ console.log(JSON.stringify(playlists, null, 2));
141
+ return;
142
+ }
143
+ if (playlists.length === 0) {
144
+ console.log(`No radio found for track ${trackId}.`);
145
+ return;
146
+ }
147
+ console.log(`\nRadio for track ${trackId}:\n`);
148
+ for (const p of playlists) {
149
+ console.log(` [${p.id}] ${p.name ?? 'Radio Mix'}${p.numberOfItems ? ` (${p.numberOfItems} tracks)` : ''}`);
150
+ }
151
+ console.log();
132
152
  }
133
- console.log(`\nRadio for track ${trackId}:\n`);
134
- for (const p of playlists) {
135
- console.log(` [${p.id}] ${p.name ?? 'Radio Mix'}${p.numberOfItems ? ` (${p.numberOfItems} tracks)` : ''}`);
153
+ catch (err) {
154
+ console.error(`Error: ${err.message}`);
155
+ process.exit(1);
136
156
  }
137
- console.log();
138
157
  }
139
- async function getTrackByIsrc(isrc, json) {
140
- const client = await (0, auth_1.getApiClient)();
158
+ async function getTrackByIsrcData(isrc, client, countryCode) {
141
159
  const { data, error } = await client.GET('/tracks', {
142
160
  params: {
143
161
  query: {
144
- countryCode: await (0, auth_1.getCountryCode)(),
162
+ countryCode,
145
163
  'filter[isrc]': [isrc],
146
164
  include: ['artists'],
147
165
  },
148
166
  },
149
167
  });
150
168
  if (error || !data) {
151
- console.error(`Error: Failed to get track by ISRC — ${JSON.stringify(error)}`);
152
- process.exit(1);
169
+ throw new Error(`Failed to get track by ISRC — ${JSON.stringify(error)}`);
153
170
  }
154
171
  const items = data.data ?? [];
155
172
  const included = data.included ?? [];
156
- const tracks = items.map((item) => {
173
+ return items.map((item) => {
157
174
  const attrs = item.attributes;
158
175
  const artistRels = item.relationships?.artists?.data ?? [];
159
176
  const artistNames = artistRels.map((rel) => {
@@ -169,41 +186,50 @@ async function getTrackByIsrc(isrc, json) {
169
186
  popularity: attrs?.popularity,
170
187
  };
171
188
  });
172
- if (json) {
173
- console.log(JSON.stringify(tracks, null, 2));
174
- return;
175
- }
176
- if (tracks.length === 0) {
177
- console.log(`No tracks found for ISRC ${isrc}.`);
178
- return;
189
+ }
190
+ async function getTrackByIsrc(isrc, json) {
191
+ const client = await (0, auth_1.getApiClient)();
192
+ const countryCode = await (0, auth_1.getCountryCode)();
193
+ try {
194
+ const tracks = await getTrackByIsrcData(isrc, client, countryCode);
195
+ if (json) {
196
+ console.log(JSON.stringify(tracks, null, 2));
197
+ return;
198
+ }
199
+ if (tracks.length === 0) {
200
+ console.log(`No tracks found for ISRC ${isrc}.`);
201
+ return;
202
+ }
203
+ console.log(`\nTracks matching ISRC ${isrc}:\n`);
204
+ for (const t of tracks) {
205
+ const artistStr = t.artists.length > 0 ? ` by ${t.artists.join(', ')}` : '';
206
+ const extras = [t.duration, t.popularity !== undefined ? `popularity: ${t.popularity}` : undefined]
207
+ .filter(Boolean)
208
+ .join(', ');
209
+ console.log(` [${t.id}] ${t.title}${artistStr}${extras ? ` (${extras})` : ''}`);
210
+ }
211
+ console.log();
179
212
  }
180
- console.log(`\nTracks matching ISRC ${isrc}:\n`);
181
- for (const t of tracks) {
182
- const artistStr = t.artists.length > 0 ? ` by ${t.artists.join(', ')}` : '';
183
- const extras = [t.duration, t.popularity !== undefined ? `popularity: ${t.popularity}` : undefined]
184
- .filter(Boolean)
185
- .join(', ');
186
- console.log(` [${t.id}] ${t.title}${artistStr}${extras ? ` (${extras})` : ''}`);
213
+ catch (err) {
214
+ console.error(`Error: ${err.message}`);
215
+ process.exit(1);
187
216
  }
188
- console.log();
189
217
  }
190
- async function getSimilarTracks(trackId, json) {
191
- const client = await (0, auth_1.getApiClient)();
218
+ async function getSimilarTracksData(trackId, client, countryCode) {
192
219
  const { data, error } = await client.GET('/tracks/{id}/relationships/similarTracks', {
193
220
  params: {
194
221
  path: { id: trackId },
195
222
  query: {
196
- countryCode: await (0, auth_1.getCountryCode)(),
223
+ countryCode,
197
224
  include: ['similarTracks'],
198
225
  },
199
226
  },
200
227
  });
201
228
  if (error || !data) {
202
- console.error(`Error: Failed to get similar tracks — ${JSON.stringify(error)}`);
203
- process.exit(1);
229
+ throw new Error(`Failed to get similar tracks — ${JSON.stringify(error)}`);
204
230
  }
205
231
  const included = data.included ?? [];
206
- const tracks = included
232
+ return included
207
233
  .filter((item) => item.type === 'tracks')
208
234
  .map((item) => {
209
235
  const attrs = item.attributes;
@@ -215,21 +241,32 @@ async function getSimilarTracks(trackId, json) {
215
241
  popularity: attrs?.popularity,
216
242
  };
217
243
  });
218
- if (json) {
219
- console.log(JSON.stringify(tracks, null, 2));
220
- return;
221
- }
222
- if (tracks.length === 0) {
223
- console.log(`No similar tracks found for track ${trackId}.`);
224
- return;
244
+ }
245
+ async function getSimilarTracks(trackId, json) {
246
+ const client = await (0, auth_1.getApiClient)();
247
+ const countryCode = await (0, auth_1.getCountryCode)();
248
+ try {
249
+ const tracks = await getSimilarTracksData(trackId, client, countryCode);
250
+ if (json) {
251
+ console.log(JSON.stringify(tracks, null, 2));
252
+ return;
253
+ }
254
+ if (tracks.length === 0) {
255
+ console.log(`No similar tracks found for track ${trackId}.`);
256
+ return;
257
+ }
258
+ console.log(`\nSimilar tracks to ${trackId}:\n`);
259
+ for (const t of tracks) {
260
+ const extras = [t.duration, t.popularity !== undefined ? `popularity: ${t.popularity}` : undefined]
261
+ .filter(Boolean)
262
+ .join(', ');
263
+ console.log(` [${t.id}] ${t.title}${extras ? ` (${extras})` : ''}`);
264
+ }
265
+ console.log();
225
266
  }
226
- console.log(`\nSimilar tracks to ${trackId}:\n`);
227
- for (const t of tracks) {
228
- const extras = [t.duration, t.popularity !== undefined ? `popularity: ${t.popularity}` : undefined]
229
- .filter(Boolean)
230
- .join(', ');
231
- console.log(` [${t.id}] ${t.title}${extras ? ` (${extras})` : ''}`);
267
+ catch (err) {
268
+ console.error(`Error: ${err.message}`);
269
+ process.exit(1);
232
270
  }
233
- console.log();
234
271
  }
235
272
  //# sourceMappingURL=track.js.map
@@ -0,0 +1,158 @@
1
+ export type SearchType = 'artist' | 'album' | 'track' | 'video' | 'playlist';
2
+ export interface SearchResult {
3
+ id: string;
4
+ type: string;
5
+ name: string;
6
+ extra?: Record<string, unknown>;
7
+ }
8
+ export interface SearchSuggestionsResult {
9
+ suggestions: string[];
10
+ directHits: Array<{
11
+ id: string;
12
+ type: string;
13
+ name: string;
14
+ }>;
15
+ }
16
+ export interface ArtistInfo {
17
+ id: string;
18
+ name: string;
19
+ popularity?: number;
20
+ handle?: string;
21
+ biography?: string;
22
+ }
23
+ export interface ArtistTrack {
24
+ id: string;
25
+ title: string;
26
+ duration: string;
27
+ isrc?: string;
28
+ popularity?: number;
29
+ }
30
+ export interface ArtistAlbum {
31
+ id: string;
32
+ title: string;
33
+ albumType?: string;
34
+ releaseDate?: string;
35
+ numberOfItems?: number;
36
+ }
37
+ export interface SimilarArtist {
38
+ id: string;
39
+ name: string;
40
+ popularity?: number;
41
+ }
42
+ export interface RadioPlaylist {
43
+ id: string;
44
+ type?: string;
45
+ name: string;
46
+ numberOfItems?: number;
47
+ }
48
+ export interface TrackInfo {
49
+ id: string;
50
+ title: string;
51
+ artists: string[];
52
+ album?: string;
53
+ duration: string;
54
+ isrc?: string;
55
+ bpm?: number;
56
+ key?: string;
57
+ popularity?: number;
58
+ explicit?: boolean;
59
+ coverUrl?: string;
60
+ }
61
+ export interface SimilarTrack {
62
+ id: string;
63
+ title: string;
64
+ duration: string;
65
+ isrc?: string;
66
+ popularity?: number;
67
+ }
68
+ export interface AlbumInfo {
69
+ id: string;
70
+ title: string;
71
+ artists: string[];
72
+ albumType?: string;
73
+ releaseDate?: string;
74
+ numberOfItems?: number;
75
+ duration: string;
76
+ popularity?: number;
77
+ explicit?: boolean;
78
+ barcodeId?: string;
79
+ coverUrl?: string;
80
+ }
81
+ export interface AlbumResult {
82
+ id: string;
83
+ title: string;
84
+ albumType?: string;
85
+ releaseDate?: string;
86
+ numberOfItems?: number;
87
+ duration: string;
88
+ barcodeId?: string;
89
+ }
90
+ export interface PlaylistInfo {
91
+ id: string;
92
+ name: string;
93
+ description?: string;
94
+ numberOfItems?: number;
95
+ createdAt?: string;
96
+ lastModifiedAt?: string;
97
+ }
98
+ export interface PlaybackInfo {
99
+ trackId: string;
100
+ presentation?: string;
101
+ previewReason?: string;
102
+ audioQuality?: string;
103
+ formats?: string[];
104
+ manifestMimeType?: string;
105
+ trackReplayGain?: number;
106
+ trackPeakAmplitude?: number;
107
+ albumReplayGain?: number;
108
+ albumPeakAmplitude?: number;
109
+ }
110
+ export interface PlaybackUrl {
111
+ trackId: string;
112
+ audioQuality?: string;
113
+ url?: string;
114
+ type?: 'direct' | 'dash';
115
+ initUrl?: string;
116
+ segmentCount?: number;
117
+ }
118
+ export interface UserProfile {
119
+ id: string;
120
+ username: string;
121
+ country: string;
122
+ email?: string;
123
+ }
124
+ export type MixCategory = 'daily' | 'discovery' | 'new-release' | 'offline';
125
+ export interface RecommendationItem {
126
+ id: string;
127
+ type: string;
128
+ name: string;
129
+ category?: MixCategory;
130
+ }
131
+ export interface MixItem {
132
+ id: string;
133
+ type: string;
134
+ name: string;
135
+ }
136
+ export interface RecentItem {
137
+ id: string;
138
+ name: string;
139
+ addedAt?: string;
140
+ }
141
+ export interface SearchHistoryEntry {
142
+ id: string;
143
+ query: string;
144
+ }
145
+ export type SavedItemType = 'tracks' | 'albums' | 'artists' | 'playlists' | 'videos';
146
+ export interface SavedItem {
147
+ id: string;
148
+ type: string;
149
+ name: string;
150
+ }
151
+ export interface ShareLink {
152
+ id: string;
153
+ code: string;
154
+ createdAt?: string;
155
+ url?: string;
156
+ }
157
+ export type LibraryResourceType = 'artist' | 'album' | 'track' | 'video';
158
+ export type RecentType = 'tracks' | 'albums' | 'artists';
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ // Shared types for *Data() functions used by both CLI and MCP server
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ //# sourceMappingURL=types.js.map
package/dist/user.d.ts CHANGED
@@ -1 +1,4 @@
1
+ import type { UserProfile } from './types';
2
+ export type { UserProfile };
3
+ export declare function getUserProfileData(client: any): Promise<UserProfile>;
1
4
  export declare function getUserProfile(json: boolean): Promise<void>;
package/dist/user.js CHANGED
@@ -1,36 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getUserProfileData = getUserProfileData;
3
4
  exports.getUserProfile = getUserProfile;
4
5
  const auth_1 = require("./auth");
5
- async function getUserProfile(json) {
6
- const client = await (0, auth_1.getApiClient)();
6
+ async function getUserProfileData(client) {
7
7
  const { data, error } = await client.GET('/users/me', {
8
8
  params: {},
9
9
  });
10
10
  if (error || !data) {
11
- console.error(`Error: Failed to get user profile — ${JSON.stringify(error)}`);
12
- process.exit(1);
11
+ throw new Error(`Failed to get user profile — ${JSON.stringify(error)}`);
13
12
  }
14
13
  const attrs = data.data?.attributes ?? {};
15
- const result = {
14
+ return {
16
15
  id: data.data?.id,
17
16
  username: attrs.username,
18
17
  country: attrs.country,
19
18
  email: attrs.email,
20
19
  };
21
- if (json) {
22
- console.log(JSON.stringify(result, null, 2));
23
- return;
20
+ }
21
+ async function getUserProfile(json) {
22
+ const client = await (0, auth_1.getApiClient)();
23
+ try {
24
+ const result = await getUserProfileData(client);
25
+ if (json) {
26
+ console.log(JSON.stringify(result, null, 2));
27
+ return;
28
+ }
29
+ console.log('\nUser profile:');
30
+ if (result.id)
31
+ console.log(` ID: ${result.id}`);
32
+ if (result.username)
33
+ console.log(` Username: ${result.username}`);
34
+ if (result.country)
35
+ console.log(` Country: ${result.country}`);
36
+ if (result.email)
37
+ console.log(` Email: ${result.email}`);
38
+ console.log();
39
+ }
40
+ catch (err) {
41
+ console.error(`Error: ${err.message}`);
42
+ process.exit(1);
24
43
  }
25
- console.log('\nUser profile:');
26
- if (result.id)
27
- console.log(` ID: ${result.id}`);
28
- if (result.username)
29
- console.log(` Username: ${result.username}`);
30
- if (result.country)
31
- console.log(` Country: ${result.country}`);
32
- if (result.email)
33
- console.log(` Email: ${result.email}`);
34
- console.log();
35
44
  }
36
45
  //# sourceMappingURL=user.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucaperret/tidal-cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.2",
4
4
  "description": "CLI for Tidal music streaming service, designed for LLM agent automation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -35,6 +35,10 @@
35
35
  "bugs": {
36
36
  "url": "https://github.com/lucaperret/tidal-cli/issues"
37
37
  },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "provenance": true
41
+ },
38
42
  "type": "commonjs",
39
43
  "engines": {
40
44
  "node": ">=20"
@@ -47,13 +51,15 @@
47
51
  "LICENSE"
48
52
  ],
49
53
  "dependencies": {
50
- "@tidal-music/api": "^0.9.1",
51
- "@tidal-music/auth": "^1.4.0",
54
+ "@tidal-music/api": "^0.22.0",
55
+ "@tidal-music/auth": "^1.6.0",
52
56
  "commander": "^14.0.3"
53
57
  },
54
58
  "devDependencies": {
55
- "@types/node": "^25.5.0",
56
- "typescript": "^5.9.3",
57
- "vitest": "^4.1.0"
59
+ "@modelcontextprotocol/sdk": "^1.29.0",
60
+ "@types/node": "^25.6.0",
61
+ "typescript": "^6.0.3",
62
+ "vitest": "^4.1.5",
63
+ "zod": "^4.4.3"
58
64
  }
59
65
  }