@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/dist/track.js ADDED
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTrackInfo = getTrackInfo;
4
+ exports.getTrackRadio = getTrackRadio;
5
+ exports.getTrackByIsrc = getTrackByIsrc;
6
+ exports.getSimilarTracks = getSimilarTracks;
7
+ const auth_1 = require("./auth");
8
+ function formatDuration(isoDuration) {
9
+ if (!isoDuration)
10
+ return '';
11
+ const match = isoDuration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
12
+ if (!match)
13
+ return isoDuration;
14
+ const h = match[1] ? `${match[1]}:` : '';
15
+ const m = (match[2] ?? '0').padStart(h ? 2 : 1, '0');
16
+ const s = (match[3] ?? '0').padStart(2, '0');
17
+ return `${h}${m}:${s}`;
18
+ }
19
+ async function getTrackInfo(trackId, json) {
20
+ const client = await (0, auth_1.getApiClient)();
21
+ const { data, error } = await client.GET('/tracks/{id}', {
22
+ params: {
23
+ path: { id: trackId },
24
+ query: {
25
+ countryCode: await (0, auth_1.getCountryCode)(),
26
+ include: ['artists', 'albums'],
27
+ },
28
+ },
29
+ });
30
+ if (error || !data) {
31
+ console.error(`Error: Failed to get track info — ${JSON.stringify(error)}`);
32
+ process.exit(1);
33
+ }
34
+ const attrs = data.data?.attributes ?? {};
35
+ const included = data.included ?? [];
36
+ const artists = included
37
+ .filter((item) => item.type === 'artists')
38
+ .map((item) => item.attributes?.name ?? item.id);
39
+ const album = included.find((item) => item.type === 'albums');
40
+ const albumName = album?.attributes?.title ?? undefined;
41
+ // Fetch cover art if we have an album
42
+ let coverUrl;
43
+ if (album?.id) {
44
+ try {
45
+ const { data: artData } = await client.GET('/albums/{id}/relationships/coverArt', {
46
+ params: {
47
+ path: { id: album.id },
48
+ query: { countryCode: await (0, auth_1.getCountryCode)(), include: ['coverArt'] },
49
+ },
50
+ });
51
+ const artwork = (artData?.included ?? []).find((i) => i.type === 'artworks');
52
+ const files = artwork?.attributes?.files ?? [];
53
+ // Pick 640x640 or the largest available
54
+ const preferred = files.find((f) => f.meta?.width === 640) ?? files[0];
55
+ coverUrl = preferred?.href;
56
+ }
57
+ catch { }
58
+ }
59
+ const result = {
60
+ id: trackId,
61
+ title: attrs.title ?? 'Unknown',
62
+ artists,
63
+ album: albumName,
64
+ duration: formatDuration(attrs.duration),
65
+ isrc: attrs.isrc,
66
+ bpm: attrs.bpm,
67
+ key: attrs.key,
68
+ popularity: attrs.popularity,
69
+ explicit: attrs.explicit,
70
+ coverUrl,
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
+ }
97
+ async function getTrackRadio(trackId, json) {
98
+ const client = await (0, auth_1.getApiClient)();
99
+ const { data, error } = await client.GET('/tracks/{id}/relationships/radio', {
100
+ params: {
101
+ path: { id: trackId },
102
+ query: {
103
+ countryCode: await (0, auth_1.getCountryCode)(),
104
+ include: ['radio'],
105
+ },
106
+ },
107
+ });
108
+ if (error || !data) {
109
+ console.error(`Error: Failed to get track radio — ${JSON.stringify(error)}`);
110
+ process.exit(1);
111
+ }
112
+ // Radio returns playlists (mix playlists), not individual tracks
113
+ const radioData = data.data ?? [];
114
+ const included = data.included ?? [];
115
+ const playlists = radioData.map((item) => {
116
+ const incl = included.find((i) => i.id === item.id && i.type === 'playlists');
117
+ const attrs = incl?.attributes ?? {};
118
+ return {
119
+ id: item.id,
120
+ type: item.type,
121
+ name: attrs.name,
122
+ numberOfItems: attrs.numberOfItems,
123
+ };
124
+ });
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;
132
+ }
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)` : ''}`);
136
+ }
137
+ console.log();
138
+ }
139
+ async function getTrackByIsrc(isrc, json) {
140
+ const client = await (0, auth_1.getApiClient)();
141
+ const { data, error } = await client.GET('/tracks', {
142
+ params: {
143
+ query: {
144
+ countryCode: await (0, auth_1.getCountryCode)(),
145
+ 'filter[isrc]': [isrc],
146
+ include: ['artists'],
147
+ },
148
+ },
149
+ });
150
+ if (error || !data) {
151
+ console.error(`Error: Failed to get track by ISRC — ${JSON.stringify(error)}`);
152
+ process.exit(1);
153
+ }
154
+ const items = data.data ?? [];
155
+ const included = data.included ?? [];
156
+ const tracks = items.map((item) => {
157
+ const attrs = item.attributes;
158
+ const artistRels = item.relationships?.artists?.data ?? [];
159
+ const artistNames = artistRels.map((rel) => {
160
+ const artist = included.find((inc) => inc.type === 'artists' && inc.id === rel.id);
161
+ return artist?.attributes?.name ?? rel.id;
162
+ });
163
+ return {
164
+ id: item.id,
165
+ title: attrs?.title ?? 'Unknown',
166
+ artists: artistNames,
167
+ duration: formatDuration(attrs?.duration),
168
+ isrc: attrs?.isrc,
169
+ popularity: attrs?.popularity,
170
+ };
171
+ });
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;
179
+ }
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})` : ''}`);
187
+ }
188
+ console.log();
189
+ }
190
+ async function getSimilarTracks(trackId, json) {
191
+ const client = await (0, auth_1.getApiClient)();
192
+ const { data, error } = await client.GET('/tracks/{id}/relationships/similarTracks', {
193
+ params: {
194
+ path: { id: trackId },
195
+ query: {
196
+ countryCode: await (0, auth_1.getCountryCode)(),
197
+ include: ['similarTracks'],
198
+ },
199
+ },
200
+ });
201
+ if (error || !data) {
202
+ console.error(`Error: Failed to get similar tracks — ${JSON.stringify(error)}`);
203
+ process.exit(1);
204
+ }
205
+ const included = data.included ?? [];
206
+ const tracks = included
207
+ .filter((item) => item.type === 'tracks')
208
+ .map((item) => {
209
+ const attrs = item.attributes;
210
+ return {
211
+ id: item.id,
212
+ title: attrs?.title ?? 'Unknown',
213
+ duration: formatDuration(attrs?.duration),
214
+ isrc: attrs?.isrc,
215
+ popularity: attrs?.popularity,
216
+ };
217
+ });
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;
225
+ }
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})` : ''}`);
232
+ }
233
+ console.log();
234
+ }
235
+ //# sourceMappingURL=track.js.map
package/dist/user.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function getUserProfile(json: boolean): Promise<void>;
package/dist/user.js ADDED
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getUserProfile = getUserProfile;
4
+ const auth_1 = require("./auth");
5
+ async function getUserProfile(json) {
6
+ const client = await (0, auth_1.getApiClient)();
7
+ const { data, error } = await client.GET('/users/me', {
8
+ params: {},
9
+ });
10
+ if (error || !data) {
11
+ console.error(`Error: Failed to get user profile — ${JSON.stringify(error)}`);
12
+ process.exit(1);
13
+ }
14
+ const attrs = data.data?.attributes ?? {};
15
+ const result = {
16
+ id: data.data?.id,
17
+ username: attrs.username,
18
+ country: attrs.country,
19
+ email: attrs.email,
20
+ };
21
+ if (json) {
22
+ console.log(JSON.stringify(result, null, 2));
23
+ return;
24
+ }
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
+ }
36
+ //# sourceMappingURL=user.js.map
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@lucaperret/tidal-cli",
3
+ "version": "1.1.0",
4
+ "description": "CLI for Tidal music streaming service, designed for LLM agent automation",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "tidal-cli": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "tidal",
18
+ "music",
19
+ "cli",
20
+ "streaming",
21
+ "llm",
22
+ "agent",
23
+ "automation",
24
+ "openclaw",
25
+ "playlist",
26
+ "hifi"
27
+ ],
28
+ "author": "Luca Perret",
29
+ "license": "MIT",
30
+ "homepage": "https://github.com/lucaperret/tidal-cli",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/lucaperret/tidal-cli.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/lucaperret/tidal-cli/issues"
37
+ },
38
+ "type": "commonjs",
39
+ "engines": {
40
+ "node": ">=20"
41
+ },
42
+ "files": [
43
+ "dist/**/*.js",
44
+ "dist/**/*.d.ts",
45
+ "!dist/__tests__",
46
+ "README.md",
47
+ "LICENSE"
48
+ ],
49
+ "dependencies": {
50
+ "@tidal-music/api": "^0.9.1",
51
+ "@tidal-music/auth": "^1.4.0",
52
+ "commander": "^14.0.3"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^25.5.0",
56
+ "typescript": "^5.9.3",
57
+ "vitest": "^4.1.0"
58
+ }
59
+ }