@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luca Perret
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# tidal-cli
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://tidal-cli.lucaperret.ch">
|
|
5
|
+
<img src="https://tidal-cli.lucaperret.ch/banner" alt="tidal-cli — Control Tidal from your terminal" width="100%" />
|
|
6
|
+
</a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/@lucaperret/tidal-cli)
|
|
10
|
+
[](https://github.com/lucaperret/tidal-cli/actions)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](https://nodejs.org)
|
|
13
|
+
|
|
14
|
+
## About
|
|
15
|
+
|
|
16
|
+
tidal-cli wraps the [Tidal API v2](https://developer.tidal.com) into a single command-line tool. Search the catalog, manage playlists, explore artists, play tracks, and handle your library — all without opening a browser. Every command supports `--json` output, making it the backbone for LLM agent automation through [OpenClaw](https://openclaw.ai).
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Search** artists, albums, tracks, videos, playlists, and autocomplete suggestions
|
|
21
|
+
- **Artists** — info, top tracks, discography, similar artists, radio
|
|
22
|
+
- **Albums** — details, barcode lookup
|
|
23
|
+
- **Tracks** — info, similar tracks, ISRC lookup, radio
|
|
24
|
+
- **Playlists** — full CRUD, add/remove tracks, reorder, add entire albums
|
|
25
|
+
- **Library** — favorites for artists, albums, tracks, videos, playlists
|
|
26
|
+
- **Playback** — stream info, direct URLs, local playback via DASH
|
|
27
|
+
- **Recommendations** — personalized mixes (My Mix, Discovery, New Arrivals)
|
|
28
|
+
- **History** — recently added tracks, albums, artists
|
|
29
|
+
- **JSON output** on every command for scripting and agent use
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g @lucaperret/tidal-cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Requirements
|
|
38
|
+
|
|
39
|
+
- Node.js >= 20
|
|
40
|
+
- A [Tidal](https://tidal.com) account
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Sign in once — opens your browser
|
|
46
|
+
tidal-cli auth
|
|
47
|
+
|
|
48
|
+
# Search for a track
|
|
49
|
+
tidal-cli search track "Around the World"
|
|
50
|
+
|
|
51
|
+
# Get artist details
|
|
52
|
+
tidal-cli artist info 8992
|
|
53
|
+
|
|
54
|
+
# Play a track
|
|
55
|
+
tidal-cli playback play 5756235
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
### Search
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
tidal-cli search artist "Gorillaz"
|
|
64
|
+
tidal-cli search album "Mezzanine"
|
|
65
|
+
tidal-cli search track "Teardrop"
|
|
66
|
+
tidal-cli search video "Stylo"
|
|
67
|
+
tidal-cli search playlist "Electronic"
|
|
68
|
+
tidal-cli search suggest "daft punk"
|
|
69
|
+
tidal-cli search editorial "indie rock"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Artist
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
tidal-cli artist info <id>
|
|
76
|
+
tidal-cli artist tracks <id>
|
|
77
|
+
tidal-cli artist albums <id>
|
|
78
|
+
tidal-cli artist similar <id>
|
|
79
|
+
tidal-cli artist radio <id>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Album & Track
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
tidal-cli album info <id>
|
|
86
|
+
tidal-cli album barcode <ean>
|
|
87
|
+
tidal-cli track info <id>
|
|
88
|
+
tidal-cli track similar <id>
|
|
89
|
+
tidal-cli track isrc <isrc>
|
|
90
|
+
tidal-cli track radio <id>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Playlists
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
tidal-cli playlist list
|
|
97
|
+
tidal-cli playlist create --name "Late Night Electronic"
|
|
98
|
+
tidal-cli playlist add-track --playlist-id <id> --track-id <id>
|
|
99
|
+
tidal-cli playlist add-album --playlist-id <id> --album-id <id>
|
|
100
|
+
tidal-cli playlist remove-track --playlist-id <id> --track-id <id>
|
|
101
|
+
tidal-cli playlist move-track --playlist-id <id> --track-id <id> --before <itemId>
|
|
102
|
+
tidal-cli playlist rename --playlist-id <id> --name "New Name"
|
|
103
|
+
tidal-cli playlist set-description --playlist-id <id> --desc "Updated description"
|
|
104
|
+
tidal-cli playlist delete --playlist-id <id>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Library
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
tidal-cli library add --track-id <id>
|
|
111
|
+
tidal-cli library add --artist-id <id>
|
|
112
|
+
tidal-cli library add --album-id <id>
|
|
113
|
+
tidal-cli library add --video-id <id>
|
|
114
|
+
tidal-cli library remove --track-id <id>
|
|
115
|
+
tidal-cli library favorite-playlists
|
|
116
|
+
tidal-cli library add-playlist --playlist-id <id>
|
|
117
|
+
tidal-cli library remove-playlist --playlist-id <id>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Discovery & History
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
tidal-cli recommend
|
|
124
|
+
tidal-cli history tracks
|
|
125
|
+
tidal-cli history albums
|
|
126
|
+
tidal-cli history artists
|
|
127
|
+
tidal-cli user profile
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Playback
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
tidal-cli playback play <id>
|
|
134
|
+
tidal-cli playback play <id> --quality LOSSLESS
|
|
135
|
+
tidal-cli playback info <id>
|
|
136
|
+
tidal-cli playback url <id>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Quality options: `LOW`, `HIGH`, `LOSSLESS`, `HI_RES`.
|
|
140
|
+
|
|
141
|
+
## JSON Output
|
|
142
|
+
|
|
143
|
+
Add `--json` before any subcommand:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
tidal-cli --json search track "Around the World"
|
|
147
|
+
tidal-cli --json playlist list
|
|
148
|
+
tidal-cli --json artist similar 8992
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Agent Automation
|
|
152
|
+
|
|
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:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
clawhub install tidal-cli
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
After `tidal-cli auth`, agents can run commands non-interactively with auto-refreshing tokens.
|
|
160
|
+
|
|
161
|
+
### Example prompts for your AI agent
|
|
162
|
+
|
|
163
|
+
- "Create a playlist with the best tracks from Daft Punk's Discovery album"
|
|
164
|
+
- "Find artists similar to Massive Attack and add their top tracks to my library"
|
|
165
|
+
- "What are my playlists? Add the new LCD Soundsystem album to the first one"
|
|
166
|
+
- "Play me something by Boards of Canada"
|
|
167
|
+
- "Build a 2000s indie rock playlist with The Strokes, Arctic Monkeys, and Interpol"
|
|
168
|
+
|
|
169
|
+
### Scripting patterns
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Search then act
|
|
173
|
+
TRACK=$(tidal-cli --json search track "Around the World" | jq -r '.[0].id')
|
|
174
|
+
tidal-cli playlist add-track --playlist-id <id> --track-id "$TRACK"
|
|
175
|
+
|
|
176
|
+
# Discovery: artist → similar → top tracks → playlist
|
|
177
|
+
ARTIST=$(tidal-cli --json search artist "Boards of Canada" | jq -r '.[0].id')
|
|
178
|
+
SIMILAR=$(tidal-cli --json artist similar "$ARTIST" | jq -r '.[0].id')
|
|
179
|
+
TRACK=$(tidal-cli --json artist tracks "$SIMILAR" | jq -r '.[0].id')
|
|
180
|
+
tidal-cli playlist add-track --playlist-id <id> --track-id "$TRACK"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Development
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
git clone https://github.com/lucaperret/tidal-cli.git
|
|
187
|
+
cd tidal-cli
|
|
188
|
+
npm install
|
|
189
|
+
npm run build
|
|
190
|
+
npm test
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Running Tests
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npm test # run once
|
|
197
|
+
npm run test:watch # watch mode
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
111 tests covering search, playlists, artists, tracks, albums, library, auth, and session.
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
tidal-cli is licensed under the MIT License. See the [`LICENSE`](LICENSE) file for details.
|
package/dist/album.d.ts
ADDED
package/dist/album.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getAlbumInfo = getAlbumInfo;
|
|
4
|
+
exports.getAlbumByBarcode = getAlbumByBarcode;
|
|
5
|
+
const auth_1 = require("./auth");
|
|
6
|
+
function formatDuration(isoDuration) {
|
|
7
|
+
if (!isoDuration)
|
|
8
|
+
return '';
|
|
9
|
+
const match = isoDuration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
|
|
10
|
+
if (!match)
|
|
11
|
+
return isoDuration;
|
|
12
|
+
const h = match[1] ? `${match[1]}:` : '';
|
|
13
|
+
const m = (match[2] ?? '0').padStart(h ? 2 : 1, '0');
|
|
14
|
+
const s = (match[3] ?? '0').padStart(2, '0');
|
|
15
|
+
return `${h}${m}:${s}`;
|
|
16
|
+
}
|
|
17
|
+
async function getAlbumInfo(albumId, json) {
|
|
18
|
+
const client = await (0, auth_1.getApiClient)();
|
|
19
|
+
const { data, error } = await client.GET('/albums/{id}', {
|
|
20
|
+
params: {
|
|
21
|
+
path: { id: albumId },
|
|
22
|
+
query: {
|
|
23
|
+
countryCode: await (0, auth_1.getCountryCode)(),
|
|
24
|
+
include: ['artists', 'coverArt'],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
if (error || !data) {
|
|
29
|
+
console.error(`Error: Failed to get album info — ${JSON.stringify(error)}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const attrs = data.data?.attributes ?? {};
|
|
33
|
+
const included = data.included ?? [];
|
|
34
|
+
const artists = included
|
|
35
|
+
.filter((item) => item.type === 'artists')
|
|
36
|
+
.map((item) => item.attributes?.name ?? item.id);
|
|
37
|
+
// Get cover art from included artworks
|
|
38
|
+
const artwork = included.find((item) => item.type === 'artworks');
|
|
39
|
+
const files = artwork?.attributes?.files ?? [];
|
|
40
|
+
const preferred = files.find((f) => f.meta?.width === 640) ?? files[0];
|
|
41
|
+
const coverUrl = preferred?.href;
|
|
42
|
+
const result = {
|
|
43
|
+
id: albumId,
|
|
44
|
+
title: attrs.title ?? 'Unknown',
|
|
45
|
+
artists,
|
|
46
|
+
albumType: attrs.albumType,
|
|
47
|
+
releaseDate: attrs.releaseDate,
|
|
48
|
+
numberOfItems: attrs.numberOfItems,
|
|
49
|
+
duration: formatDuration(attrs.duration),
|
|
50
|
+
popularity: attrs.popularity,
|
|
51
|
+
explicit: attrs.explicit,
|
|
52
|
+
barcodeId: attrs.barcodeId,
|
|
53
|
+
coverUrl,
|
|
54
|
+
};
|
|
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
|
+
}
|
|
80
|
+
async function getAlbumByBarcode(barcode, json) {
|
|
81
|
+
const client = await (0, auth_1.getApiClient)();
|
|
82
|
+
const { data, error } = await client.GET('/albums', {
|
|
83
|
+
params: {
|
|
84
|
+
query: {
|
|
85
|
+
countryCode: await (0, auth_1.getCountryCode)(),
|
|
86
|
+
'filter[barcodeId]': [barcode],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
if (error || !data) {
|
|
91
|
+
console.error(`Error: Failed to get album by barcode — ${JSON.stringify(error)}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const items = data.data ?? [];
|
|
95
|
+
const albums = items.map((item) => {
|
|
96
|
+
const attrs = item.attributes;
|
|
97
|
+
return {
|
|
98
|
+
id: item.id,
|
|
99
|
+
title: attrs?.title ?? 'Unknown',
|
|
100
|
+
albumType: attrs?.albumType,
|
|
101
|
+
releaseDate: attrs?.releaseDate,
|
|
102
|
+
numberOfItems: attrs?.numberOfItems,
|
|
103
|
+
duration: formatDuration(attrs?.duration),
|
|
104
|
+
barcodeId: attrs?.barcodeId,
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
if (json) {
|
|
108
|
+
console.log(JSON.stringify(albums, null, 2));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (albums.length === 0) {
|
|
112
|
+
console.log(`No albums found for barcode ${barcode}.`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
console.log(`\nAlbums matching barcode ${barcode}:\n`);
|
|
116
|
+
for (const a of albums) {
|
|
117
|
+
const extras = [a.albumType, a.releaseDate, a.numberOfItems !== undefined ? `${a.numberOfItems} tracks` : undefined]
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join(', ');
|
|
120
|
+
console.log(` [${a.id}] ${a.title}${extras ? ` (${extras})` : ''}`);
|
|
121
|
+
}
|
|
122
|
+
console.log();
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=album.js.map
|
package/dist/artist.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function getArtistInfo(artistId: string, json: boolean): Promise<void>;
|
|
2
|
+
export declare function getArtistRadio(artistId: string, json: boolean): Promise<void>;
|
|
3
|
+
export declare function getArtistTracks(artistId: string, json: boolean): Promise<void>;
|
|
4
|
+
export declare function getArtistAlbums(artistId: string, json: boolean): Promise<void>;
|
|
5
|
+
export declare function getSimilarArtists(artistId: string, json: boolean): Promise<void>;
|
package/dist/artist.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getArtistInfo = getArtistInfo;
|
|
4
|
+
exports.getArtistRadio = getArtistRadio;
|
|
5
|
+
exports.getArtistTracks = getArtistTracks;
|
|
6
|
+
exports.getArtistAlbums = getArtistAlbums;
|
|
7
|
+
exports.getSimilarArtists = getSimilarArtists;
|
|
8
|
+
const auth_1 = require("./auth");
|
|
9
|
+
function formatDuration(isoDuration) {
|
|
10
|
+
if (!isoDuration)
|
|
11
|
+
return '';
|
|
12
|
+
const match = isoDuration.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
|
|
13
|
+
if (!match)
|
|
14
|
+
return isoDuration;
|
|
15
|
+
const h = match[1] ? `${match[1]}:` : '';
|
|
16
|
+
const m = (match[2] ?? '0').padStart(h ? 2 : 1, '0');
|
|
17
|
+
const s = (match[3] ?? '0').padStart(2, '0');
|
|
18
|
+
return `${h}${m}:${s}`;
|
|
19
|
+
}
|
|
20
|
+
async function getArtistInfo(artistId, json) {
|
|
21
|
+
const client = await (0, auth_1.getApiClient)();
|
|
22
|
+
const { data, error } = await client.GET('/artists/{id}', {
|
|
23
|
+
params: {
|
|
24
|
+
path: { id: artistId },
|
|
25
|
+
query: {
|
|
26
|
+
countryCode: await (0, auth_1.getCountryCode)(),
|
|
27
|
+
include: ['biography'],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
if (error || !data) {
|
|
32
|
+
console.error(`Error: Failed to get artist info — ${JSON.stringify(error)}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const attrs = data.data?.attributes ?? {};
|
|
36
|
+
const included = data.included ?? [];
|
|
37
|
+
const biographyItem = included.find((item) => item.type === 'artistBiographies');
|
|
38
|
+
const biographyText = biographyItem?.attributes?.text ?? attrs.biography?.text ?? attrs.biography;
|
|
39
|
+
const result = {
|
|
40
|
+
id: artistId,
|
|
41
|
+
name: attrs.name ?? 'Unknown',
|
|
42
|
+
popularity: attrs.popularity,
|
|
43
|
+
handle: attrs.handle,
|
|
44
|
+
biography: biographyText,
|
|
45
|
+
};
|
|
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
|
+
}
|
|
59
|
+
async function getArtistRadio(artistId, json) {
|
|
60
|
+
const client = await (0, auth_1.getApiClient)();
|
|
61
|
+
const { data, error } = await client.GET('/artists/{id}/relationships/radio', {
|
|
62
|
+
params: {
|
|
63
|
+
path: { id: artistId },
|
|
64
|
+
query: {
|
|
65
|
+
countryCode: await (0, auth_1.getCountryCode)(),
|
|
66
|
+
include: ['radio'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
if (error || !data) {
|
|
71
|
+
console.error(`Error: Failed to get artist radio — ${JSON.stringify(error)}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
// Radio returns playlists (mix playlists), not individual tracks
|
|
75
|
+
const radioData = data.data ?? [];
|
|
76
|
+
const included = data.included ?? [];
|
|
77
|
+
const playlists = radioData.map((item) => {
|
|
78
|
+
const incl = included.find((i) => i.id === item.id && i.type === 'playlists');
|
|
79
|
+
const attrs = incl?.attributes ?? {};
|
|
80
|
+
return {
|
|
81
|
+
id: item.id,
|
|
82
|
+
type: item.type,
|
|
83
|
+
name: attrs.name,
|
|
84
|
+
numberOfItems: attrs.numberOfItems,
|
|
85
|
+
description: attrs.description,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
if (json) {
|
|
89
|
+
console.log(JSON.stringify(playlists, null, 2));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (playlists.length === 0) {
|
|
93
|
+
console.log(`No radio found for artist ${artistId}.`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.log(`\nRadio for artist ${artistId}:\n`);
|
|
97
|
+
for (const p of playlists) {
|
|
98
|
+
console.log(` [${p.id}] ${p.name ?? 'Radio Mix'}${p.numberOfItems ? ` (${p.numberOfItems} tracks)` : ''}`);
|
|
99
|
+
}
|
|
100
|
+
console.log();
|
|
101
|
+
}
|
|
102
|
+
async function getArtistTracks(artistId, json) {
|
|
103
|
+
const client = await (0, auth_1.getApiClient)();
|
|
104
|
+
const { data, error } = await client.GET('/artists/{id}/relationships/tracks', {
|
|
105
|
+
params: {
|
|
106
|
+
path: { id: artistId },
|
|
107
|
+
query: {
|
|
108
|
+
countryCode: await (0, auth_1.getCountryCode)(),
|
|
109
|
+
'collapseBy': 'FINGERPRINT',
|
|
110
|
+
include: ['tracks'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
if (error || !data) {
|
|
115
|
+
console.error(`Error: Failed to get artist tracks — ${JSON.stringify(error)}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
const included = data.included ?? [];
|
|
119
|
+
const tracks = included
|
|
120
|
+
.filter((item) => item.type === 'tracks')
|
|
121
|
+
.map((item) => {
|
|
122
|
+
const attrs = item.attributes;
|
|
123
|
+
return {
|
|
124
|
+
id: item.id,
|
|
125
|
+
title: attrs?.title ?? 'Unknown',
|
|
126
|
+
duration: attrs?.duration,
|
|
127
|
+
isrc: attrs?.isrc,
|
|
128
|
+
popularity: attrs?.popularity,
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
if (json) {
|
|
132
|
+
console.log(JSON.stringify(tracks, null, 2));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (tracks.length === 0) {
|
|
136
|
+
console.log(`No tracks found for artist ${artistId}.`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
console.log(`\nTracks for artist ${artistId}:\n`);
|
|
140
|
+
for (const t of tracks) {
|
|
141
|
+
console.log(` [${t.id}] ${t.title}${t.popularity !== undefined ? ` (popularity: ${t.popularity})` : ''}`);
|
|
142
|
+
}
|
|
143
|
+
console.log();
|
|
144
|
+
}
|
|
145
|
+
async function getArtistAlbums(artistId, json) {
|
|
146
|
+
const client = await (0, auth_1.getApiClient)();
|
|
147
|
+
const { data, error } = await client.GET('/artists/{id}/relationships/albums', {
|
|
148
|
+
params: {
|
|
149
|
+
path: { id: artistId },
|
|
150
|
+
query: {
|
|
151
|
+
countryCode: await (0, auth_1.getCountryCode)(),
|
|
152
|
+
include: ['albums'],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
if (error || !data) {
|
|
157
|
+
console.error(`Error: Failed to get artist albums — ${JSON.stringify(error)}`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
const included = data.included ?? [];
|
|
161
|
+
const albums = included
|
|
162
|
+
.filter((item) => item.type === 'albums')
|
|
163
|
+
.map((item) => {
|
|
164
|
+
const attrs = item.attributes;
|
|
165
|
+
return {
|
|
166
|
+
id: item.id,
|
|
167
|
+
title: attrs?.title ?? 'Unknown',
|
|
168
|
+
albumType: attrs?.albumType,
|
|
169
|
+
releaseDate: attrs?.releaseDate,
|
|
170
|
+
numberOfItems: attrs?.numberOfItems,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
if (json) {
|
|
174
|
+
console.log(JSON.stringify(albums, null, 2));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (albums.length === 0) {
|
|
178
|
+
console.log(`No albums found for artist ${artistId}.`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
console.log(`\nAlbums for artist ${artistId}:\n`);
|
|
182
|
+
for (const a of albums) {
|
|
183
|
+
const extras = [a.albumType, a.releaseDate, a.numberOfItems !== undefined ? `${a.numberOfItems} tracks` : undefined]
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.join(', ');
|
|
186
|
+
console.log(` [${a.id}] ${a.title}${extras ? ` (${extras})` : ''}`);
|
|
187
|
+
}
|
|
188
|
+
console.log();
|
|
189
|
+
}
|
|
190
|
+
async function getSimilarArtists(artistId, json) {
|
|
191
|
+
const client = await (0, auth_1.getApiClient)();
|
|
192
|
+
const { data, error } = await client.GET('/artists/{id}/relationships/similarArtists', {
|
|
193
|
+
params: {
|
|
194
|
+
path: { id: artistId },
|
|
195
|
+
query: {
|
|
196
|
+
countryCode: await (0, auth_1.getCountryCode)(),
|
|
197
|
+
include: ['similarArtists'],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
if (error || !data) {
|
|
202
|
+
console.error(`Error: Failed to get similar artists — ${JSON.stringify(error)}`);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
const included = data.included ?? [];
|
|
206
|
+
const artists = included
|
|
207
|
+
.filter((item) => item.type === 'artists')
|
|
208
|
+
.map((item) => {
|
|
209
|
+
const attrs = item.attributes;
|
|
210
|
+
return {
|
|
211
|
+
id: item.id,
|
|
212
|
+
name: attrs?.name ?? 'Unknown',
|
|
213
|
+
popularity: attrs?.popularity,
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
if (json) {
|
|
217
|
+
console.log(JSON.stringify(artists, null, 2));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (artists.length === 0) {
|
|
221
|
+
console.log(`No similar artists found for artist ${artistId}.`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
console.log(`\nSimilar artists to ${artistId}:\n`);
|
|
225
|
+
for (const a of artists) {
|
|
226
|
+
console.log(` [${a.id}] ${a.name}${a.popularity !== undefined ? ` (popularity: ${a.popularity})` : ''}`);
|
|
227
|
+
}
|
|
228
|
+
console.log();
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=artist.js.map
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function ensureInit(): Promise<void>;
|
|
2
|
+
export declare function authenticate(): Promise<void>;
|
|
3
|
+
export declare function getApiClient(): Promise<any>;
|
|
4
|
+
export declare function getCountryCode(): Promise<string>;
|
|
5
|
+
export declare function doLogout(): Promise<void>;
|