@lucaperret/tidal-cli 1.1.2 → 1.2.3
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 +34 -5
- 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 +8 -6
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/@lucaperret/tidal-cli)
|
|
10
10
|
[](https://github.com/lucaperret/tidal-cli/actions)
|
|
11
|
+
[](https://smithery.ai/servers/lucaperret/tidal)
|
|
11
12
|
[](LICENSE)
|
|
12
13
|
[](https://nodejs.org)
|
|
13
14
|
|
|
@@ -24,8 +25,10 @@ tidal-cli wraps the [Tidal API v2](https://developer.tidal.com) into a single co
|
|
|
24
25
|
- **Playlists** — full CRUD, add/remove tracks, reorder, add entire albums
|
|
25
26
|
- **Library** — favorites for artists, albums, tracks, videos, playlists
|
|
26
27
|
- **Playback** — stream info, direct URLs, local playback via DASH
|
|
27
|
-
- **Recommendations** — personalized mixes (
|
|
28
|
-
- **History** — recently added tracks, albums, artists
|
|
28
|
+
- **Recommendations** — personalized mixes (Daily, Discovery, New Release, Offline) with drill-down into mix items
|
|
29
|
+
- **History** — recently added tracks, albums, artists; search history (list, delete, clear)
|
|
30
|
+
- **Save for Later** — bookmark items in a separate queue from your main library
|
|
31
|
+
- **Sharing** — generate public share links for tracks and albums
|
|
29
32
|
- **JSON output** on every command for scripting and agent use
|
|
30
33
|
|
|
31
34
|
## Installation
|
|
@@ -120,13 +123,28 @@ tidal-cli library remove-playlist --playlist-id <id>
|
|
|
120
123
|
### Discovery & History
|
|
121
124
|
|
|
122
125
|
```bash
|
|
123
|
-
tidal-cli recommend
|
|
126
|
+
tidal-cli recommend # all mix categories
|
|
127
|
+
tidal-cli recommend --type daily # daily | discovery | new-release | offline
|
|
128
|
+
tidal-cli mix items <mix-id> --type daily # tracks inside a specific mix
|
|
124
129
|
tidal-cli history tracks
|
|
125
130
|
tidal-cli history albums
|
|
126
131
|
tidal-cli history artists
|
|
132
|
+
tidal-cli search history # your recent searches
|
|
133
|
+
tidal-cli search history-delete <entry-id>
|
|
134
|
+
tidal-cli search history-clear
|
|
127
135
|
tidal-cli user profile
|
|
128
136
|
```
|
|
129
137
|
|
|
138
|
+
### Save for Later & Sharing
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
tidal-cli saved list
|
|
142
|
+
tidal-cli saved add --type tracks --id <id> # tracks | albums | artists | playlists | videos
|
|
143
|
+
tidal-cli saved remove --type albums --id <id>
|
|
144
|
+
tidal-cli share track <id> # creates a public share link
|
|
145
|
+
tidal-cli share album <id>
|
|
146
|
+
```
|
|
147
|
+
|
|
130
148
|
### Playback
|
|
131
149
|
|
|
132
150
|
```bash
|
|
@@ -148,9 +166,20 @@ tidal-cli --json playlist list
|
|
|
148
166
|
tidal-cli --json artist similar 8992
|
|
149
167
|
```
|
|
150
168
|
|
|
169
|
+
## MCP Server (Claude Integration)
|
|
170
|
+
|
|
171
|
+
tidal-cli is available as a remote MCP server for [Claude Desktop](https://claude.ai), [Smithery](https://smithery.ai/servers/lucaperret/tidal), and any MCP-compatible client.
|
|
172
|
+
|
|
173
|
+
**Connect in Claude Desktop:**
|
|
174
|
+
1. Settings → Connectors → Add custom connector
|
|
175
|
+
2. Enter: `https://tidal-cli.lucaperret.ch/api/mcp`
|
|
176
|
+
3. Click "Connect" → log in to Tidal → done
|
|
177
|
+
|
|
178
|
+
40 tools with OAuth authentication, safety annotations, and 3 prompt templates.
|
|
179
|
+
|
|
151
180
|
## Agent Automation
|
|
152
181
|
|
|
153
|
-
tidal-cli is available as an [OpenClaw](https://openclaw.ai) skill on [ClawHub](https://clawhub.ai/lucaperret/tidal-cli). Install it for your AI agent:
|
|
182
|
+
tidal-cli is also available as an [OpenClaw](https://openclaw.ai) skill on [ClawHub](https://clawhub.ai/lucaperret/tidal-cli). Install it for your AI agent:
|
|
154
183
|
|
|
155
184
|
```bash
|
|
156
185
|
clawhub install tidal-cli
|
|
@@ -197,7 +226,7 @@ npm test # run once
|
|
|
197
226
|
npm run test:watch # watch mode
|
|
198
227
|
```
|
|
199
228
|
|
|
200
|
-
|
|
229
|
+
143 tests covering search, playlists, artists, tracks, albums, library, recommendations, mixes, save-for-later, sharing, search history, auth, and session.
|
|
201
230
|
|
|
202
231
|
## License
|
|
203
232
|
|
package/dist/album.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
import type { AlbumInfo, AlbumResult } from './types';
|
|
2
|
+
export type { AlbumInfo, AlbumResult };
|
|
3
|
+
export declare function getAlbumInfoData(albumId: string, client: any, countryCode: string): Promise<AlbumInfo>;
|
|
1
4
|
export declare function getAlbumInfo(albumId: string, json: boolean): Promise<void>;
|
|
5
|
+
export declare function getAlbumByBarcodeData(barcode: string, client: any, countryCode: string): Promise<AlbumResult[]>;
|
|
2
6
|
export declare function getAlbumByBarcode(barcode: string, json: boolean): Promise<void>;
|
package/dist/album.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getAlbumInfoData = getAlbumInfoData;
|
|
3
4
|
exports.getAlbumInfo = getAlbumInfo;
|
|
5
|
+
exports.getAlbumByBarcodeData = getAlbumByBarcodeData;
|
|
4
6
|
exports.getAlbumByBarcode = getAlbumByBarcode;
|
|
5
7
|
const auth_1 = require("./auth");
|
|
6
8
|
function formatDuration(isoDuration) {
|
|
@@ -14,32 +16,29 @@ function formatDuration(isoDuration) {
|
|
|
14
16
|
const s = (match[3] ?? '0').padStart(2, '0');
|
|
15
17
|
return `${h}${m}:${s}`;
|
|
16
18
|
}
|
|
17
|
-
async function
|
|
18
|
-
const client = await (0, auth_1.getApiClient)();
|
|
19
|
+
async function getAlbumInfoData(albumId, client, countryCode) {
|
|
19
20
|
const { data, error } = await client.GET('/albums/{id}', {
|
|
20
21
|
params: {
|
|
21
22
|
path: { id: albumId },
|
|
22
23
|
query: {
|
|
23
|
-
countryCode
|
|
24
|
+
countryCode,
|
|
24
25
|
include: ['artists', 'coverArt'],
|
|
25
26
|
},
|
|
26
27
|
},
|
|
27
28
|
});
|
|
28
29
|
if (error || !data) {
|
|
29
|
-
|
|
30
|
-
process.exit(1);
|
|
30
|
+
throw new Error(`Failed to get album info — ${JSON.stringify(error)}`);
|
|
31
31
|
}
|
|
32
32
|
const attrs = data.data?.attributes ?? {};
|
|
33
33
|
const included = data.included ?? [];
|
|
34
34
|
const artists = included
|
|
35
35
|
.filter((item) => item.type === 'artists')
|
|
36
36
|
.map((item) => item.attributes?.name ?? item.id);
|
|
37
|
-
// Get cover art from included artworks
|
|
38
37
|
const artwork = included.find((item) => item.type === 'artworks');
|
|
39
38
|
const files = artwork?.attributes?.files ?? [];
|
|
40
39
|
const preferred = files.find((f) => f.meta?.width === 640) ?? files[0];
|
|
41
40
|
const coverUrl = preferred?.href;
|
|
42
|
-
|
|
41
|
+
return {
|
|
43
42
|
id: albumId,
|
|
44
43
|
title: attrs.title ?? 'Unknown',
|
|
45
44
|
artists,
|
|
@@ -52,47 +51,56 @@ async function getAlbumInfo(albumId, json) {
|
|
|
52
51
|
barcodeId: attrs.barcodeId,
|
|
53
52
|
coverUrl,
|
|
54
53
|
};
|
|
55
|
-
if (json) {
|
|
56
|
-
console.log(JSON.stringify(result, null, 2));
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
console.log(`\nAlbum: [${result.id}] ${result.title}`);
|
|
60
|
-
if (result.artists.length > 0)
|
|
61
|
-
console.log(` Artists: ${result.artists.join(', ')}`);
|
|
62
|
-
if (result.albumType)
|
|
63
|
-
console.log(` Type: ${result.albumType}`);
|
|
64
|
-
if (result.releaseDate)
|
|
65
|
-
console.log(` Release Date: ${result.releaseDate}`);
|
|
66
|
-
if (result.numberOfItems !== undefined)
|
|
67
|
-
console.log(` Tracks: ${result.numberOfItems}`);
|
|
68
|
-
if (result.duration)
|
|
69
|
-
console.log(` Duration: ${result.duration}`);
|
|
70
|
-
if (result.popularity !== undefined)
|
|
71
|
-
console.log(` Popularity: ${result.popularity}`);
|
|
72
|
-
if (result.explicit !== undefined)
|
|
73
|
-
console.log(` Explicit: ${result.explicit}`);
|
|
74
|
-
if (result.barcodeId)
|
|
75
|
-
console.log(` Barcode: ${result.barcodeId}`);
|
|
76
|
-
if (result.coverUrl)
|
|
77
|
-
console.log(` Cover: ${result.coverUrl}`);
|
|
78
|
-
console.log();
|
|
79
54
|
}
|
|
80
|
-
async function
|
|
55
|
+
async function getAlbumInfo(albumId, json) {
|
|
81
56
|
const client = await (0, auth_1.getApiClient)();
|
|
57
|
+
const countryCode = await (0, auth_1.getCountryCode)();
|
|
58
|
+
try {
|
|
59
|
+
const result = await getAlbumInfoData(albumId, client, countryCode);
|
|
60
|
+
if (json) {
|
|
61
|
+
console.log(JSON.stringify(result, null, 2));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.log(`\nAlbum: [${result.id}] ${result.title}`);
|
|
65
|
+
if (result.artists.length > 0)
|
|
66
|
+
console.log(` Artists: ${result.artists.join(', ')}`);
|
|
67
|
+
if (result.albumType)
|
|
68
|
+
console.log(` Type: ${result.albumType}`);
|
|
69
|
+
if (result.releaseDate)
|
|
70
|
+
console.log(` Release Date: ${result.releaseDate}`);
|
|
71
|
+
if (result.numberOfItems !== undefined)
|
|
72
|
+
console.log(` Tracks: ${result.numberOfItems}`);
|
|
73
|
+
if (result.duration)
|
|
74
|
+
console.log(` Duration: ${result.duration}`);
|
|
75
|
+
if (result.popularity !== undefined)
|
|
76
|
+
console.log(` Popularity: ${result.popularity}`);
|
|
77
|
+
if (result.explicit !== undefined)
|
|
78
|
+
console.log(` Explicit: ${result.explicit}`);
|
|
79
|
+
if (result.barcodeId)
|
|
80
|
+
console.log(` Barcode: ${result.barcodeId}`);
|
|
81
|
+
if (result.coverUrl)
|
|
82
|
+
console.log(` Cover: ${result.coverUrl}`);
|
|
83
|
+
console.log();
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.error(`Error: ${err.message}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function getAlbumByBarcodeData(barcode, client, countryCode) {
|
|
82
91
|
const { data, error } = await client.GET('/albums', {
|
|
83
92
|
params: {
|
|
84
93
|
query: {
|
|
85
|
-
countryCode
|
|
94
|
+
countryCode,
|
|
86
95
|
'filter[barcodeId]': [barcode],
|
|
87
96
|
},
|
|
88
97
|
},
|
|
89
98
|
});
|
|
90
99
|
if (error || !data) {
|
|
91
|
-
|
|
92
|
-
process.exit(1);
|
|
100
|
+
throw new Error(`Failed to get album by barcode — ${JSON.stringify(error)}`);
|
|
93
101
|
}
|
|
94
102
|
const items = data.data ?? [];
|
|
95
|
-
|
|
103
|
+
return items.map((item) => {
|
|
96
104
|
const attrs = item.attributes;
|
|
97
105
|
return {
|
|
98
106
|
id: item.id,
|
|
@@ -104,21 +112,32 @@ async function getAlbumByBarcode(barcode, json) {
|
|
|
104
112
|
barcodeId: attrs?.barcodeId,
|
|
105
113
|
};
|
|
106
114
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
}
|
|
116
|
+
async function getAlbumByBarcode(barcode, json) {
|
|
117
|
+
const client = await (0, auth_1.getApiClient)();
|
|
118
|
+
const countryCode = await (0, auth_1.getCountryCode)();
|
|
119
|
+
try {
|
|
120
|
+
const albums = await getAlbumByBarcodeData(barcode, client, countryCode);
|
|
121
|
+
if (json) {
|
|
122
|
+
console.log(JSON.stringify(albums, null, 2));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (albums.length === 0) {
|
|
126
|
+
console.log(`No albums found for barcode ${barcode}.`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
console.log(`\nAlbums matching barcode ${barcode}:\n`);
|
|
130
|
+
for (const a of albums) {
|
|
131
|
+
const extras = [a.albumType, a.releaseDate, a.numberOfItems !== undefined ? `${a.numberOfItems} tracks` : undefined]
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join(', ');
|
|
134
|
+
console.log(` [${a.id}] ${a.title}${extras ? ` (${extras})` : ''}`);
|
|
135
|
+
}
|
|
136
|
+
console.log();
|
|
114
137
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.filter(Boolean)
|
|
119
|
-
.join(', ');
|
|
120
|
-
console.log(` [${a.id}] ${a.title}${extras ? ` (${extras})` : ''}`);
|
|
138
|
+
catch (err) {
|
|
139
|
+
console.error(`Error: ${err.message}`);
|
|
140
|
+
process.exit(1);
|
|
121
141
|
}
|
|
122
|
-
console.log();
|
|
123
142
|
}
|
|
124
143
|
//# sourceMappingURL=album.js.map
|
package/dist/artist.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import type { ArtistInfo, ArtistTrack, ArtistAlbum, SimilarArtist, RadioPlaylist } from './types';
|
|
2
|
+
export type { ArtistInfo, ArtistTrack, ArtistAlbum, SimilarArtist };
|
|
3
|
+
export declare function getArtistInfoData(artistId: string, client: any, countryCode: string): Promise<ArtistInfo>;
|
|
1
4
|
export declare function getArtistInfo(artistId: string, json: boolean): Promise<void>;
|
|
5
|
+
export declare function getArtistRadioData(artistId: string, client: any, countryCode: string): Promise<RadioPlaylist[]>;
|
|
2
6
|
export declare function getArtistRadio(artistId: string, json: boolean): Promise<void>;
|
|
7
|
+
export declare function getArtistTracksData(artistId: string, client: any, countryCode: string): Promise<ArtistTrack[]>;
|
|
3
8
|
export declare function getArtistTracks(artistId: string, json: boolean): Promise<void>;
|
|
9
|
+
export declare function getArtistAlbumsData(artistId: string, client: any, countryCode: string): Promise<ArtistAlbum[]>;
|
|
4
10
|
export declare function getArtistAlbums(artistId: string, json: boolean): Promise<void>;
|
|
11
|
+
export declare function getSimilarArtistsData(artistId: string, client: any, countryCode: string): Promise<SimilarArtist[]>;
|
|
5
12
|
export declare function getSimilarArtists(artistId: string, json: boolean): Promise<void>;
|
package/dist/artist.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getArtistInfoData = getArtistInfoData;
|
|
3
4
|
exports.getArtistInfo = getArtistInfo;
|
|
5
|
+
exports.getArtistRadioData = getArtistRadioData;
|
|
4
6
|
exports.getArtistRadio = getArtistRadio;
|
|
7
|
+
exports.getArtistTracksData = getArtistTracksData;
|
|
5
8
|
exports.getArtistTracks = getArtistTracks;
|
|
9
|
+
exports.getArtistAlbumsData = getArtistAlbumsData;
|
|
6
10
|
exports.getArtistAlbums = getArtistAlbums;
|
|
11
|
+
exports.getSimilarArtistsData = getSimilarArtistsData;
|
|
7
12
|
exports.getSimilarArtists = getSimilarArtists;
|
|
8
13
|
const auth_1 = require("./auth");
|
|
9
14
|
function formatDuration(isoDuration) {
|
|
@@ -17,64 +22,70 @@ function formatDuration(isoDuration) {
|
|
|
17
22
|
const s = (match[3] ?? '0').padStart(2, '0');
|
|
18
23
|
return `${h}${m}:${s}`;
|
|
19
24
|
}
|
|
20
|
-
async function
|
|
21
|
-
const client = await (0, auth_1.getApiClient)();
|
|
25
|
+
async function getArtistInfoData(artistId, client, countryCode) {
|
|
22
26
|
const { data, error } = await client.GET('/artists/{id}', {
|
|
23
27
|
params: {
|
|
24
28
|
path: { id: artistId },
|
|
25
29
|
query: {
|
|
26
|
-
countryCode
|
|
30
|
+
countryCode,
|
|
27
31
|
include: ['biography'],
|
|
28
32
|
},
|
|
29
33
|
},
|
|
30
34
|
});
|
|
31
35
|
if (error || !data) {
|
|
32
|
-
|
|
33
|
-
process.exit(1);
|
|
36
|
+
throw new Error(`Failed to get artist info — ${JSON.stringify(error)}`);
|
|
34
37
|
}
|
|
35
38
|
const attrs = data.data?.attributes ?? {};
|
|
36
39
|
const included = data.included ?? [];
|
|
37
40
|
const biographyItem = included.find((item) => item.type === 'artistBiographies');
|
|
38
41
|
const biographyText = biographyItem?.attributes?.text ?? attrs.biography?.text ?? attrs.biography;
|
|
39
|
-
|
|
42
|
+
return {
|
|
40
43
|
id: artistId,
|
|
41
44
|
name: attrs.name ?? 'Unknown',
|
|
42
45
|
popularity: attrs.popularity,
|
|
43
46
|
handle: attrs.handle,
|
|
44
47
|
biography: biographyText,
|
|
45
48
|
};
|
|
46
|
-
if (json) {
|
|
47
|
-
console.log(JSON.stringify(result, null, 2));
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
console.log(`\nArtist: [${result.id}] ${result.name}`);
|
|
51
|
-
if (result.handle)
|
|
52
|
-
console.log(` Handle: ${result.handle}`);
|
|
53
|
-
if (result.popularity !== undefined)
|
|
54
|
-
console.log(` Popularity: ${result.popularity}`);
|
|
55
|
-
if (result.biography)
|
|
56
|
-
console.log(` Biography: ${result.biography}`);
|
|
57
|
-
console.log();
|
|
58
49
|
}
|
|
59
|
-
async function
|
|
50
|
+
async function getArtistInfo(artistId, json) {
|
|
60
51
|
const client = await (0, auth_1.getApiClient)();
|
|
52
|
+
const countryCode = await (0, auth_1.getCountryCode)();
|
|
53
|
+
try {
|
|
54
|
+
const result = await getArtistInfoData(artistId, client, countryCode);
|
|
55
|
+
if (json) {
|
|
56
|
+
console.log(JSON.stringify(result, null, 2));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
console.log(`\nArtist: [${result.id}] ${result.name}`);
|
|
60
|
+
if (result.handle)
|
|
61
|
+
console.log(` Handle: ${result.handle}`);
|
|
62
|
+
if (result.popularity !== undefined)
|
|
63
|
+
console.log(` Popularity: ${result.popularity}`);
|
|
64
|
+
if (result.biography)
|
|
65
|
+
console.log(` Biography: ${result.biography}`);
|
|
66
|
+
console.log();
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.error(`Error: ${err.message}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function getArtistRadioData(artistId, client, countryCode) {
|
|
61
74
|
const { data, error } = await client.GET('/artists/{id}/relationships/radio', {
|
|
62
75
|
params: {
|
|
63
76
|
path: { id: artistId },
|
|
64
77
|
query: {
|
|
65
|
-
countryCode
|
|
78
|
+
countryCode,
|
|
66
79
|
include: ['radio'],
|
|
67
80
|
},
|
|
68
81
|
},
|
|
69
82
|
});
|
|
70
83
|
if (error || !data) {
|
|
71
|
-
|
|
72
|
-
process.exit(1);
|
|
84
|
+
throw new Error(`Failed to get artist radio — ${JSON.stringify(error)}`);
|
|
73
85
|
}
|
|
74
|
-
// Radio returns playlists (mix playlists), not individual tracks
|
|
75
86
|
const radioData = data.data ?? [];
|
|
76
87
|
const included = data.included ?? [];
|
|
77
|
-
|
|
88
|
+
return radioData.map((item) => {
|
|
78
89
|
const incl = included.find((i) => i.id === item.id && i.type === 'playlists');
|
|
79
90
|
const attrs = incl?.attributes ?? {};
|
|
80
91
|
return {
|
|
@@ -82,83 +93,100 @@ async function getArtistRadio(artistId, json) {
|
|
|
82
93
|
type: item.type,
|
|
83
94
|
name: attrs.name,
|
|
84
95
|
numberOfItems: attrs.numberOfItems,
|
|
85
|
-
description: attrs.description,
|
|
86
96
|
};
|
|
87
97
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
}
|
|
99
|
+
async function getArtistRadio(artistId, json) {
|
|
100
|
+
const client = await (0, auth_1.getApiClient)();
|
|
101
|
+
const countryCode = await (0, auth_1.getCountryCode)();
|
|
102
|
+
try {
|
|
103
|
+
const playlists = await getArtistRadioData(artistId, client, countryCode);
|
|
104
|
+
if (json) {
|
|
105
|
+
console.log(JSON.stringify(playlists, null, 2));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (playlists.length === 0) {
|
|
109
|
+
console.log(`No radio found for artist ${artistId}.`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log(`\nRadio for artist ${artistId}:\n`);
|
|
113
|
+
for (const p of playlists) {
|
|
114
|
+
console.log(` [${p.id}] ${p.name ?? 'Radio Mix'}${p.numberOfItems ? ` (${p.numberOfItems} tracks)` : ''}`);
|
|
115
|
+
}
|
|
116
|
+
console.log();
|
|
95
117
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
118
|
+
catch (err) {
|
|
119
|
+
console.error(`Error: ${err.message}`);
|
|
120
|
+
process.exit(1);
|
|
99
121
|
}
|
|
100
|
-
console.log();
|
|
101
122
|
}
|
|
102
|
-
async function
|
|
103
|
-
const client = await (0, auth_1.getApiClient)();
|
|
123
|
+
async function getArtistTracksData(artistId, client, countryCode) {
|
|
104
124
|
const { data, error } = await client.GET('/artists/{id}/relationships/tracks', {
|
|
105
125
|
params: {
|
|
106
126
|
path: { id: artistId },
|
|
107
127
|
query: {
|
|
108
|
-
countryCode
|
|
128
|
+
countryCode,
|
|
109
129
|
'collapseBy': 'FINGERPRINT',
|
|
110
130
|
include: ['tracks'],
|
|
111
131
|
},
|
|
112
132
|
},
|
|
113
133
|
});
|
|
114
134
|
if (error || !data) {
|
|
115
|
-
|
|
116
|
-
process.exit(1);
|
|
135
|
+
throw new Error(`Failed to get artist tracks — ${JSON.stringify(error)}`);
|
|
117
136
|
}
|
|
118
137
|
const included = data.included ?? [];
|
|
119
|
-
|
|
138
|
+
return included
|
|
120
139
|
.filter((item) => item.type === 'tracks')
|
|
121
140
|
.map((item) => {
|
|
122
141
|
const attrs = item.attributes;
|
|
123
142
|
return {
|
|
124
143
|
id: item.id,
|
|
125
144
|
title: attrs?.title ?? 'Unknown',
|
|
126
|
-
duration: attrs?.duration,
|
|
145
|
+
duration: formatDuration(attrs?.duration),
|
|
127
146
|
isrc: attrs?.isrc,
|
|
128
147
|
popularity: attrs?.popularity,
|
|
129
148
|
};
|
|
130
149
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
150
|
+
}
|
|
151
|
+
async function getArtistTracks(artistId, json) {
|
|
152
|
+
const client = await (0, auth_1.getApiClient)();
|
|
153
|
+
const countryCode = await (0, auth_1.getCountryCode)();
|
|
154
|
+
try {
|
|
155
|
+
const tracks = await getArtistTracksData(artistId, client, countryCode);
|
|
156
|
+
if (json) {
|
|
157
|
+
console.log(JSON.stringify(tracks, null, 2));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (tracks.length === 0) {
|
|
161
|
+
console.log(`No tracks found for artist ${artistId}.`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
console.log(`\nTracks for artist ${artistId}:\n`);
|
|
165
|
+
for (const t of tracks) {
|
|
166
|
+
console.log(` [${t.id}] ${t.title}${t.popularity !== undefined ? ` (popularity: ${t.popularity})` : ''}`);
|
|
167
|
+
}
|
|
168
|
+
console.log();
|
|
138
169
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
170
|
+
catch (err) {
|
|
171
|
+
console.error(`Error: ${err.message}`);
|
|
172
|
+
process.exit(1);
|
|
142
173
|
}
|
|
143
|
-
console.log();
|
|
144
174
|
}
|
|
145
|
-
async function
|
|
146
|
-
const client = await (0, auth_1.getApiClient)();
|
|
175
|
+
async function getArtistAlbumsData(artistId, client, countryCode) {
|
|
147
176
|
const { data, error } = await client.GET('/artists/{id}/relationships/albums', {
|
|
148
177
|
params: {
|
|
149
178
|
path: { id: artistId },
|
|
150
179
|
query: {
|
|
151
|
-
countryCode
|
|
180
|
+
countryCode,
|
|
152
181
|
include: ['albums'],
|
|
153
182
|
},
|
|
154
183
|
},
|
|
155
184
|
});
|
|
156
185
|
if (error || !data) {
|
|
157
|
-
|
|
158
|
-
process.exit(1);
|
|
186
|
+
throw new Error(`Failed to get artist albums — ${JSON.stringify(error)}`);
|
|
159
187
|
}
|
|
160
188
|
const included = data.included ?? [];
|
|
161
|
-
|
|
189
|
+
return included
|
|
162
190
|
.filter((item) => item.type === 'albums')
|
|
163
191
|
.map((item) => {
|
|
164
192
|
const attrs = item.attributes;
|
|
@@ -170,40 +198,49 @@ async function getArtistAlbums(artistId, json) {
|
|
|
170
198
|
numberOfItems: attrs?.numberOfItems,
|
|
171
199
|
};
|
|
172
200
|
});
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
}
|
|
202
|
+
async function getArtistAlbums(artistId, json) {
|
|
203
|
+
const client = await (0, auth_1.getApiClient)();
|
|
204
|
+
const countryCode = await (0, auth_1.getCountryCode)();
|
|
205
|
+
try {
|
|
206
|
+
const albums = await getArtistAlbumsData(artistId, client, countryCode);
|
|
207
|
+
if (json) {
|
|
208
|
+
console.log(JSON.stringify(albums, null, 2));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (albums.length === 0) {
|
|
212
|
+
console.log(`No albums found for artist ${artistId}.`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
console.log(`\nAlbums for artist ${artistId}:\n`);
|
|
216
|
+
for (const a of albums) {
|
|
217
|
+
const extras = [a.albumType, a.releaseDate, a.numberOfItems !== undefined ? `${a.numberOfItems} tracks` : undefined]
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
.join(', ');
|
|
220
|
+
console.log(` [${a.id}] ${a.title}${extras ? ` (${extras})` : ''}`);
|
|
221
|
+
}
|
|
222
|
+
console.log();
|
|
180
223
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
.filter(Boolean)
|
|
185
|
-
.join(', ');
|
|
186
|
-
console.log(` [${a.id}] ${a.title}${extras ? ` (${extras})` : ''}`);
|
|
224
|
+
catch (err) {
|
|
225
|
+
console.error(`Error: ${err.message}`);
|
|
226
|
+
process.exit(1);
|
|
187
227
|
}
|
|
188
|
-
console.log();
|
|
189
228
|
}
|
|
190
|
-
async function
|
|
191
|
-
const client = await (0, auth_1.getApiClient)();
|
|
229
|
+
async function getSimilarArtistsData(artistId, client, countryCode) {
|
|
192
230
|
const { data, error } = await client.GET('/artists/{id}/relationships/similarArtists', {
|
|
193
231
|
params: {
|
|
194
232
|
path: { id: artistId },
|
|
195
233
|
query: {
|
|
196
|
-
countryCode
|
|
234
|
+
countryCode,
|
|
197
235
|
include: ['similarArtists'],
|
|
198
236
|
},
|
|
199
237
|
},
|
|
200
238
|
});
|
|
201
239
|
if (error || !data) {
|
|
202
|
-
|
|
203
|
-
process.exit(1);
|
|
240
|
+
throw new Error(`Failed to get similar artists — ${JSON.stringify(error)}`);
|
|
204
241
|
}
|
|
205
242
|
const included = data.included ?? [];
|
|
206
|
-
|
|
243
|
+
return included
|
|
207
244
|
.filter((item) => item.type === 'artists')
|
|
208
245
|
.map((item) => {
|
|
209
246
|
const attrs = item.attributes;
|
|
@@ -213,18 +250,29 @@ async function getSimilarArtists(artistId, json) {
|
|
|
213
250
|
popularity: attrs?.popularity,
|
|
214
251
|
};
|
|
215
252
|
});
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
253
|
+
}
|
|
254
|
+
async function getSimilarArtists(artistId, json) {
|
|
255
|
+
const client = await (0, auth_1.getApiClient)();
|
|
256
|
+
const countryCode = await (0, auth_1.getCountryCode)();
|
|
257
|
+
try {
|
|
258
|
+
const artists = await getSimilarArtistsData(artistId, client, countryCode);
|
|
259
|
+
if (json) {
|
|
260
|
+
console.log(JSON.stringify(artists, null, 2));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (artists.length === 0) {
|
|
264
|
+
console.log(`No similar artists found for artist ${artistId}.`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
console.log(`\nSimilar artists to ${artistId}:\n`);
|
|
268
|
+
for (const a of artists) {
|
|
269
|
+
console.log(` [${a.id}] ${a.name}${a.popularity !== undefined ? ` (popularity: ${a.popularity})` : ''}`);
|
|
270
|
+
}
|
|
271
|
+
console.log();
|
|
223
272
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
273
|
+
catch (err) {
|
|
274
|
+
console.error(`Error: ${err.message}`);
|
|
275
|
+
process.exit(1);
|
|
227
276
|
}
|
|
228
|
-
console.log();
|
|
229
277
|
}
|
|
230
278
|
//# sourceMappingURL=artist.js.map
|
package/dist/history.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
type
|
|
1
|
+
import type { RecentItem, RecentType } from './types';
|
|
2
|
+
export type { RecentItem, RecentType };
|
|
3
|
+
export declare function getRecentlyAddedData(type: RecentType, client: any, countryCode: string): Promise<RecentItem[]>;
|
|
2
4
|
export declare function getRecentlyAdded(type: RecentType, json: boolean): Promise<void>;
|
|
3
|
-
export {};
|