@kud/qobuz 0.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/README.md +52 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.js +352 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
**Reverse-engineered Qobuz API client for Node.js**
|
|
8
|
+
|
|
9
|
+
<a href="https://kud.io/projects/qobuz">Website</a> · <a href="https://kud.io/projects/qobuz/docs">Documentation</a>
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Token auth, captcha-proof** — authenticates with a `user_auth_token` borrowed from your logged-in browser; no password handling, no captcha walls.
|
|
16
|
+
- **Pluggable credential store** — ships in-memory and macOS Keychain implementations; bring your own.
|
|
17
|
+
- **Typed resources** — search, albums, artists, tracks, playlists, favourites — clean camelCase domain types mapped from the raw API.
|
|
18
|
+
- **Deep links** — build `open.qobuz.com` URLs to open anything in the Qobuz app.
|
|
19
|
+
- **ESM + types, zero runtime deps** — tree-shakeable, fully typed, ships nothing extra.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install @kud/qobuz
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Grab your credentials from a logged-in [play.qobuz.com](https://play.qobuz.com) session — open DevTools, inspect any `api.json` network request, and copy the `X-App-Id` and `X-User-Auth-Token` headers. Store them via the credential store of your choice, then create a client:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createQobuzClient, createKeychainStore } from "@kud/qobuz"
|
|
33
|
+
|
|
34
|
+
const client = await createQobuzClient({ store: createKeychainStore() })
|
|
35
|
+
const { albums } = await client.search.search("radiohead")
|
|
36
|
+
console.log(client.deepLink.album(albums[0].id))
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The companion CLI [`@kud/qobuz-cli`](https://kud.io/projects/qobuz-cli) provides a `qobuz login` flow that handles credential storage automatically.
|
|
40
|
+
|
|
41
|
+
## Development
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
git clone https://github.com/kud/qobuz.git
|
|
45
|
+
cd qobuz
|
|
46
|
+
npm install
|
|
47
|
+
npm run build # tsup → ESM + dts
|
|
48
|
+
npm test # vitest
|
|
49
|
+
npm run typecheck
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
📚 **Full documentation → [qobuz/docs](https://kud.io/projects/qobuz/docs)**
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
type DeepLinkBase = "open" | "play";
|
|
2
|
+
type DeepLink = {
|
|
3
|
+
album: (albumId: string) => string;
|
|
4
|
+
track: (trackId: number) => string;
|
|
5
|
+
playlist: (playlistId: number) => string;
|
|
6
|
+
artist: (artistId: number) => string;
|
|
7
|
+
};
|
|
8
|
+
declare const createDeepLink: (base?: DeepLinkBase) => DeepLink;
|
|
9
|
+
|
|
10
|
+
declare const QOBUZ_BASE_URL = "https://www.qobuz.com/api.json/0.2";
|
|
11
|
+
type QueryParams = Record<string, string | number | undefined>;
|
|
12
|
+
type Transport = {
|
|
13
|
+
get: <T = unknown>(path: string, query?: QueryParams) => Promise<T>;
|
|
14
|
+
};
|
|
15
|
+
type TransportConfig = {
|
|
16
|
+
appId: string;
|
|
17
|
+
token?: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
fetchImpl?: typeof fetch;
|
|
20
|
+
};
|
|
21
|
+
declare const createTransport: ({ appId, token, baseUrl, fetchImpl, }: TransportConfig) => Transport;
|
|
22
|
+
|
|
23
|
+
type QobuzImage = {
|
|
24
|
+
thumbnail?: string;
|
|
25
|
+
small?: string;
|
|
26
|
+
large?: string;
|
|
27
|
+
};
|
|
28
|
+
type Artist = {
|
|
29
|
+
id: number;
|
|
30
|
+
name: string;
|
|
31
|
+
picture?: string;
|
|
32
|
+
albumsCount?: number;
|
|
33
|
+
};
|
|
34
|
+
type Album = {
|
|
35
|
+
id: string;
|
|
36
|
+
title: string;
|
|
37
|
+
artist?: Artist;
|
|
38
|
+
tracksCount?: number;
|
|
39
|
+
releaseDate?: string;
|
|
40
|
+
duration?: number;
|
|
41
|
+
image?: QobuzImage;
|
|
42
|
+
genre?: string;
|
|
43
|
+
hires?: boolean;
|
|
44
|
+
};
|
|
45
|
+
type Track = {
|
|
46
|
+
id: number;
|
|
47
|
+
title: string;
|
|
48
|
+
album?: Album;
|
|
49
|
+
artist?: Artist;
|
|
50
|
+
trackNumber?: number;
|
|
51
|
+
duration?: number;
|
|
52
|
+
hires?: boolean;
|
|
53
|
+
};
|
|
54
|
+
type Playlist = {
|
|
55
|
+
id: number;
|
|
56
|
+
name: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
tracksCount?: number;
|
|
59
|
+
isPublic?: boolean;
|
|
60
|
+
owner?: string;
|
|
61
|
+
duration?: number;
|
|
62
|
+
};
|
|
63
|
+
type SearchResults = {
|
|
64
|
+
query: string;
|
|
65
|
+
albums: Album[];
|
|
66
|
+
tracks: Track[];
|
|
67
|
+
artists: Artist[];
|
|
68
|
+
};
|
|
69
|
+
type FavouriteType = "albums" | "artists" | "tracks";
|
|
70
|
+
type PageOptions = {
|
|
71
|
+
limit?: number;
|
|
72
|
+
offset?: number;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type CreatePlaylistParams = {
|
|
76
|
+
name: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
isPublic?: boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type UserFavourites = {
|
|
82
|
+
albums: Album[];
|
|
83
|
+
artists: Artist[];
|
|
84
|
+
tracks: Track[];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type StoredCredentials = {
|
|
88
|
+
appId: string;
|
|
89
|
+
token: string;
|
|
90
|
+
savedAt?: string;
|
|
91
|
+
};
|
|
92
|
+
type CredentialStore = {
|
|
93
|
+
load: () => Promise<StoredCredentials | undefined>;
|
|
94
|
+
save: (credentials: StoredCredentials) => Promise<void>;
|
|
95
|
+
clear: () => Promise<void>;
|
|
96
|
+
};
|
|
97
|
+
declare const createMemoryStore: (seed?: StoredCredentials) => CredentialStore;
|
|
98
|
+
type KeychainStoreOptions = {
|
|
99
|
+
service?: string;
|
|
100
|
+
account?: string;
|
|
101
|
+
};
|
|
102
|
+
declare const createKeychainStore: (options?: KeychainStoreOptions) => CredentialStore;
|
|
103
|
+
|
|
104
|
+
type QobuzClientConfig = {
|
|
105
|
+
store: CredentialStore;
|
|
106
|
+
fetchImpl?: typeof fetch;
|
|
107
|
+
};
|
|
108
|
+
declare const createQobuzClient: ({ store, fetchImpl, }: QobuzClientConfig) => Promise<{
|
|
109
|
+
appId: string;
|
|
110
|
+
search: {
|
|
111
|
+
search: (query: string, options?: PageOptions) => Promise<SearchResults>;
|
|
112
|
+
};
|
|
113
|
+
albums: {
|
|
114
|
+
get: (albumId: string) => Promise<Album>;
|
|
115
|
+
};
|
|
116
|
+
artists: {
|
|
117
|
+
get: (artistId: number) => Promise<Artist>;
|
|
118
|
+
getSimilar: (artistId: number, options?: PageOptions) => Promise<Artist[]>;
|
|
119
|
+
};
|
|
120
|
+
tracks: {
|
|
121
|
+
get: (trackId: number) => Promise<Track>;
|
|
122
|
+
};
|
|
123
|
+
favourites: {
|
|
124
|
+
list: (type: FavouriteType, options?: PageOptions) => Promise<UserFavourites>;
|
|
125
|
+
add: (type: FavouriteType, id: string) => Promise<void>;
|
|
126
|
+
remove: (type: FavouriteType, id: string) => Promise<void>;
|
|
127
|
+
};
|
|
128
|
+
playlists: {
|
|
129
|
+
listForUser: (options?: PageOptions) => Promise<Playlist[]>;
|
|
130
|
+
get: (playlistId: number, options?: PageOptions) => Promise<Playlist>;
|
|
131
|
+
create: ({ name, description, isPublic, }: CreatePlaylistParams) => Promise<Playlist>;
|
|
132
|
+
remove: (playlistId: number) => Promise<void>;
|
|
133
|
+
addTracks: (playlistId: number, trackIds: number[]) => Promise<Playlist>;
|
|
134
|
+
removeTracks: (playlistId: number, playlistTrackIds: number[]) => Promise<Playlist>;
|
|
135
|
+
};
|
|
136
|
+
deepLink: DeepLink;
|
|
137
|
+
signOut: () => Promise<void>;
|
|
138
|
+
}>;
|
|
139
|
+
type QobuzClient = Awaited<ReturnType<typeof createQobuzClient>>;
|
|
140
|
+
|
|
141
|
+
type AppCredentials = {
|
|
142
|
+
appId: string;
|
|
143
|
+
bundlePath: string;
|
|
144
|
+
};
|
|
145
|
+
declare const fetchAppId: (options?: {
|
|
146
|
+
fetchImpl?: typeof fetch;
|
|
147
|
+
}) => Promise<AppCredentials>;
|
|
148
|
+
|
|
149
|
+
type ValidateConfig = {
|
|
150
|
+
appId: string;
|
|
151
|
+
token: string;
|
|
152
|
+
fetchImpl?: typeof fetch;
|
|
153
|
+
};
|
|
154
|
+
declare const validateCredentials: ({ appId, token, fetchImpl, }: ValidateConfig) => Promise<void>;
|
|
155
|
+
|
|
156
|
+
type QobuzErrorKind = "http" | "auth" | "bootstrap";
|
|
157
|
+
type QobuzError = Error & {
|
|
158
|
+
kind: QobuzErrorKind;
|
|
159
|
+
status?: number;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export { type Album, type AppCredentials, type Artist, type CreatePlaylistParams, type CredentialStore, type DeepLink, type DeepLinkBase, type FavouriteType, type KeychainStoreOptions, type PageOptions, type Playlist, QOBUZ_BASE_URL, type QobuzClient, type QobuzClientConfig, type QobuzError, type QobuzErrorKind, type QobuzImage, type SearchResults, type StoredCredentials, type Track, type Transport, type TransportConfig, type UserFavourites, type ValidateConfig, createDeepLink, createKeychainStore, createMemoryStore, createQobuzClient, createTransport, fetchAppId, validateCredentials };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// src/deep-link.ts
|
|
2
|
+
var createDeepLink = (base = "open") => {
|
|
3
|
+
const root = `https://${base}.qobuz.com`;
|
|
4
|
+
return {
|
|
5
|
+
album: (albumId) => `${root}/album/${albumId}`,
|
|
6
|
+
track: (trackId) => `${root}/track/${trackId}`,
|
|
7
|
+
playlist: (playlistId) => `${root}/playlist/${playlistId}`,
|
|
8
|
+
artist: (artistId) => `${root}/artist/${artistId}`
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/http/errors.ts
|
|
13
|
+
var createError = (kind, message, status) => Object.assign(new Error(message), { name: "QobuzError", kind, status });
|
|
14
|
+
var httpError = (message, status) => createError("http", message, status);
|
|
15
|
+
var authError = (message) => createError("auth", message);
|
|
16
|
+
var bootstrapError = (message) => createError("bootstrap", message);
|
|
17
|
+
|
|
18
|
+
// src/http/transport.ts
|
|
19
|
+
var QOBUZ_BASE_URL = "https://www.qobuz.com/api.json/0.2";
|
|
20
|
+
var USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
21
|
+
var toQuery = (params) => new URLSearchParams(
|
|
22
|
+
Object.entries(params).filter(([, value]) => value !== void 0).map(([key, value]) => [key, String(value)])
|
|
23
|
+
);
|
|
24
|
+
var createTransport = ({
|
|
25
|
+
appId,
|
|
26
|
+
token,
|
|
27
|
+
baseUrl = QOBUZ_BASE_URL,
|
|
28
|
+
fetchImpl = fetch
|
|
29
|
+
}) => {
|
|
30
|
+
const headers = {
|
|
31
|
+
"User-Agent": USER_AGENT,
|
|
32
|
+
"X-App-Id": appId,
|
|
33
|
+
...token ? { "X-User-Auth-Token": token } : {}
|
|
34
|
+
};
|
|
35
|
+
const get = async (path, query = {}) => {
|
|
36
|
+
const res = await fetchImpl(`${baseUrl}/${path}?${toQuery(query)}`, {
|
|
37
|
+
headers
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const body = await res.text().catch(() => "");
|
|
41
|
+
throw httpError(
|
|
42
|
+
`${path} failed (${res.status}): ${body.slice(0, 200)}`,
|
|
43
|
+
res.status
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return await res.json();
|
|
47
|
+
};
|
|
48
|
+
return { get };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/mappers.ts
|
|
52
|
+
var mapImage = (raw) => raw ? { thumbnail: raw.thumbnail, small: raw.small, large: raw.large } : void 0;
|
|
53
|
+
var mapArtist = (raw) => ({
|
|
54
|
+
id: raw.id,
|
|
55
|
+
name: raw.name,
|
|
56
|
+
picture: raw.picture ?? raw.image?.medium,
|
|
57
|
+
albumsCount: raw.albums_count
|
|
58
|
+
});
|
|
59
|
+
var mapAlbum = (raw) => ({
|
|
60
|
+
id: String(raw.id),
|
|
61
|
+
title: raw.title,
|
|
62
|
+
artist: raw.artist ? mapArtist(raw.artist) : void 0,
|
|
63
|
+
tracksCount: raw.tracks_count,
|
|
64
|
+
releaseDate: raw.release_date_original ?? raw.released_at,
|
|
65
|
+
duration: raw.duration,
|
|
66
|
+
image: mapImage(raw.image),
|
|
67
|
+
genre: raw.genre?.name,
|
|
68
|
+
hires: raw.hires
|
|
69
|
+
});
|
|
70
|
+
var mapTrack = (raw) => ({
|
|
71
|
+
id: raw.id,
|
|
72
|
+
title: raw.title,
|
|
73
|
+
album: raw.album ? mapAlbum(raw.album) : void 0,
|
|
74
|
+
artist: raw.performer ? mapArtist(raw.performer) : raw.artist ? mapArtist(raw.artist) : void 0,
|
|
75
|
+
trackNumber: raw.track_number,
|
|
76
|
+
duration: raw.duration,
|
|
77
|
+
hires: raw.hires
|
|
78
|
+
});
|
|
79
|
+
var mapPlaylist = (raw) => ({
|
|
80
|
+
id: raw.id,
|
|
81
|
+
name: raw.name,
|
|
82
|
+
description: raw.description,
|
|
83
|
+
tracksCount: raw.tracks_count,
|
|
84
|
+
isPublic: raw.is_public,
|
|
85
|
+
owner: raw.owner?.name,
|
|
86
|
+
duration: raw.duration
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// src/resources/albums.ts
|
|
90
|
+
var createAlbumsResource = (transport) => ({
|
|
91
|
+
get: async (albumId) => mapAlbum(await transport.get("album/get", { album_id: albumId }))
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// src/resources/artists.ts
|
|
95
|
+
var createArtistsResource = (transport) => ({
|
|
96
|
+
get: async (artistId) => mapArtist(await transport.get("artist/get", { artist_id: artistId })),
|
|
97
|
+
getSimilar: async (artistId, options = {}) => {
|
|
98
|
+
const raw = await transport.get("artist/getSimilarArtists", {
|
|
99
|
+
artist_id: artistId,
|
|
100
|
+
limit: options.limit ?? 20,
|
|
101
|
+
offset: options.offset ?? 0
|
|
102
|
+
});
|
|
103
|
+
return (raw.artists?.items ?? []).map(mapArtist);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// src/resources/favourites.ts
|
|
108
|
+
var favouriteIdParam = {
|
|
109
|
+
albums: "album_ids",
|
|
110
|
+
artists: "artist_ids",
|
|
111
|
+
tracks: "track_ids"
|
|
112
|
+
};
|
|
113
|
+
var createFavouritesResource = (transport) => ({
|
|
114
|
+
list: async (type, options = {}) => {
|
|
115
|
+
const raw = await transport.get(
|
|
116
|
+
"favorite/getUserFavorites",
|
|
117
|
+
{
|
|
118
|
+
type,
|
|
119
|
+
limit: options.limit ?? 50,
|
|
120
|
+
offset: options.offset ?? 0
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
return {
|
|
124
|
+
albums: (raw.albums?.items ?? []).map(mapAlbum),
|
|
125
|
+
artists: (raw.artists?.items ?? []).map(mapArtist),
|
|
126
|
+
tracks: (raw.tracks?.items ?? []).map(mapTrack)
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
add: async (type, id) => {
|
|
130
|
+
await transport.get("favorite/create", { [favouriteIdParam[type]]: id });
|
|
131
|
+
},
|
|
132
|
+
remove: async (type, id) => {
|
|
133
|
+
await transport.get("favorite/delete", { [favouriteIdParam[type]]: id });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// src/resources/playlists.ts
|
|
138
|
+
var createPlaylistsResource = (transport) => ({
|
|
139
|
+
listForUser: async (options = {}) => {
|
|
140
|
+
const raw = await transport.get("playlist/getUserPlaylists", {
|
|
141
|
+
limit: options.limit ?? 50,
|
|
142
|
+
offset: options.offset ?? 0
|
|
143
|
+
});
|
|
144
|
+
return (raw.playlists?.items ?? []).map(mapPlaylist);
|
|
145
|
+
},
|
|
146
|
+
get: async (playlistId, options = {}) => mapPlaylist(
|
|
147
|
+
await transport.get("playlist/get", {
|
|
148
|
+
playlist_id: playlistId,
|
|
149
|
+
extra: "tracks",
|
|
150
|
+
limit: options.limit ?? 500,
|
|
151
|
+
offset: options.offset ?? 0
|
|
152
|
+
})
|
|
153
|
+
),
|
|
154
|
+
create: async ({
|
|
155
|
+
name,
|
|
156
|
+
description,
|
|
157
|
+
isPublic
|
|
158
|
+
}) => mapPlaylist(
|
|
159
|
+
await transport.get("playlist/create", {
|
|
160
|
+
name,
|
|
161
|
+
description,
|
|
162
|
+
is_public: isPublic ? 1 : 0
|
|
163
|
+
})
|
|
164
|
+
),
|
|
165
|
+
remove: async (playlistId) => {
|
|
166
|
+
await transport.get("playlist/delete", { playlist_id: playlistId });
|
|
167
|
+
},
|
|
168
|
+
addTracks: async (playlistId, trackIds) => mapPlaylist(
|
|
169
|
+
await transport.get("playlist/addTracks", {
|
|
170
|
+
playlist_id: playlistId,
|
|
171
|
+
track_ids: trackIds.join(",")
|
|
172
|
+
})
|
|
173
|
+
),
|
|
174
|
+
removeTracks: async (playlistId, playlistTrackIds) => mapPlaylist(
|
|
175
|
+
await transport.get("playlist/deleteTracks", {
|
|
176
|
+
playlist_id: playlistId,
|
|
177
|
+
playlist_track_ids: playlistTrackIds.join(",")
|
|
178
|
+
})
|
|
179
|
+
)
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// src/resources/search.ts
|
|
183
|
+
var createSearchResource = (transport) => ({
|
|
184
|
+
search: async (query, options = {}) => {
|
|
185
|
+
const raw = await transport.get("catalog/search", {
|
|
186
|
+
query,
|
|
187
|
+
limit: options.limit ?? 20,
|
|
188
|
+
offset: options.offset ?? 0
|
|
189
|
+
});
|
|
190
|
+
return {
|
|
191
|
+
query,
|
|
192
|
+
albums: (raw.albums?.items ?? []).map(mapAlbum),
|
|
193
|
+
tracks: (raw.tracks?.items ?? []).map(mapTrack),
|
|
194
|
+
artists: (raw.artists?.items ?? []).map(mapArtist)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// src/resources/tracks.ts
|
|
200
|
+
var createTracksResource = (transport) => ({
|
|
201
|
+
get: async (trackId) => mapTrack(await transport.get("track/get", { track_id: trackId }))
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// src/client.ts
|
|
205
|
+
var createQobuzClient = async ({
|
|
206
|
+
store,
|
|
207
|
+
fetchImpl
|
|
208
|
+
}) => {
|
|
209
|
+
const credentials = await store.load();
|
|
210
|
+
if (!credentials)
|
|
211
|
+
throw authError("not connected \u2014 store a valid app_id + token first");
|
|
212
|
+
const transport = createTransport({
|
|
213
|
+
appId: credentials.appId,
|
|
214
|
+
token: credentials.token,
|
|
215
|
+
fetchImpl
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
appId: credentials.appId,
|
|
219
|
+
search: createSearchResource(transport),
|
|
220
|
+
albums: createAlbumsResource(transport),
|
|
221
|
+
artists: createArtistsResource(transport),
|
|
222
|
+
tracks: createTracksResource(transport),
|
|
223
|
+
favourites: createFavouritesResource(transport),
|
|
224
|
+
playlists: createPlaylistsResource(transport),
|
|
225
|
+
deepLink: createDeepLink(),
|
|
226
|
+
signOut: () => store.clear()
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// src/auth/bootstrap.ts
|
|
231
|
+
var PLAY_URL = "https://play.qobuz.com";
|
|
232
|
+
var fetchAppId = async (options = {}) => {
|
|
233
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
234
|
+
const getText = async (url) => {
|
|
235
|
+
const res = await fetchImpl(url, { headers: { "User-Agent": USER_AGENT } });
|
|
236
|
+
if (!res.ok) throw bootstrapError(`GET ${url} failed (${res.status})`);
|
|
237
|
+
return res.text();
|
|
238
|
+
};
|
|
239
|
+
const loginPage = await getText(`${PLAY_URL}/login`);
|
|
240
|
+
const bundlePath = loginPage.match(
|
|
241
|
+
/<script src="(\/resources\/\d+\.\d+\.\d+-[a-z]\d{3}\/bundle\.js)"><\/script>/
|
|
242
|
+
)?.[1];
|
|
243
|
+
if (!bundlePath)
|
|
244
|
+
throw bootstrapError(
|
|
245
|
+
"could not find the bundle.js URL in the Qobuz login page"
|
|
246
|
+
);
|
|
247
|
+
const bundle = await getText(`${PLAY_URL}${bundlePath}`);
|
|
248
|
+
const appId = bundle.match(/production:\{api:\{appId:"(\d{9})"/)?.[1];
|
|
249
|
+
if (!appId)
|
|
250
|
+
throw bootstrapError("could not extract app_id from the Qobuz web bundle");
|
|
251
|
+
return { appId, bundlePath };
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/auth/validate.ts
|
|
255
|
+
var validateCredentials = async ({
|
|
256
|
+
appId,
|
|
257
|
+
token,
|
|
258
|
+
fetchImpl
|
|
259
|
+
}) => {
|
|
260
|
+
const transport = createTransport({ appId, token, fetchImpl });
|
|
261
|
+
try {
|
|
262
|
+
await transport.get("favorite/getUserFavorites", {
|
|
263
|
+
type: "albums",
|
|
264
|
+
limit: 1
|
|
265
|
+
});
|
|
266
|
+
} catch (error) {
|
|
267
|
+
const qobuzError = error;
|
|
268
|
+
if (qobuzError.status === 401) {
|
|
269
|
+
throw authError(
|
|
270
|
+
"Qobuz rejected the credentials (401) \u2014 the token may be expired or the app_id doesn't match"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/auth/credential-store.ts
|
|
278
|
+
import { execFile } from "child_process";
|
|
279
|
+
import { promisify } from "util";
|
|
280
|
+
var exec = promisify(execFile);
|
|
281
|
+
var createMemoryStore = (seed) => {
|
|
282
|
+
let current = seed;
|
|
283
|
+
return {
|
|
284
|
+
load: async () => current,
|
|
285
|
+
save: async (credentials) => {
|
|
286
|
+
current = credentials;
|
|
287
|
+
},
|
|
288
|
+
clear: async () => {
|
|
289
|
+
current = void 0;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
var createKeychainStore = (options = {}) => {
|
|
294
|
+
const service = options.service ?? "qobuz";
|
|
295
|
+
const account = options.account ?? "credentials";
|
|
296
|
+
return {
|
|
297
|
+
load: async () => {
|
|
298
|
+
try {
|
|
299
|
+
const { stdout } = await exec("security", [
|
|
300
|
+
"find-generic-password",
|
|
301
|
+
"-s",
|
|
302
|
+
service,
|
|
303
|
+
"-a",
|
|
304
|
+
account,
|
|
305
|
+
"-w"
|
|
306
|
+
]);
|
|
307
|
+
return JSON.parse(stdout.trim());
|
|
308
|
+
} catch {
|
|
309
|
+
return void 0;
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
save: async (credentials) => {
|
|
313
|
+
const value = JSON.stringify({
|
|
314
|
+
...credentials,
|
|
315
|
+
savedAt: credentials.savedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
316
|
+
});
|
|
317
|
+
await exec("security", [
|
|
318
|
+
"add-generic-password",
|
|
319
|
+
"-U",
|
|
320
|
+
"-s",
|
|
321
|
+
service,
|
|
322
|
+
"-a",
|
|
323
|
+
account,
|
|
324
|
+
"-w",
|
|
325
|
+
value
|
|
326
|
+
]);
|
|
327
|
+
},
|
|
328
|
+
clear: async () => {
|
|
329
|
+
try {
|
|
330
|
+
await exec("security", [
|
|
331
|
+
"delete-generic-password",
|
|
332
|
+
"-s",
|
|
333
|
+
service,
|
|
334
|
+
"-a",
|
|
335
|
+
account
|
|
336
|
+
]);
|
|
337
|
+
} catch {
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
export {
|
|
343
|
+
QOBUZ_BASE_URL,
|
|
344
|
+
createDeepLink,
|
|
345
|
+
createKeychainStore,
|
|
346
|
+
createMemoryStore,
|
|
347
|
+
createQobuzClient,
|
|
348
|
+
createTransport,
|
|
349
|
+
fetchAppId,
|
|
350
|
+
validateCredentials
|
|
351
|
+
};
|
|
352
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/deep-link.ts","../src/http/errors.ts","../src/http/transport.ts","../src/mappers.ts","../src/resources/albums.ts","../src/resources/artists.ts","../src/resources/favourites.ts","../src/resources/playlists.ts","../src/resources/search.ts","../src/resources/tracks.ts","../src/client.ts","../src/auth/bootstrap.ts","../src/auth/validate.ts","../src/auth/credential-store.ts"],"sourcesContent":["export type DeepLinkBase = \"open\" | \"play\"\n\nexport type DeepLink = {\n album: (albumId: string) => string\n track: (trackId: number) => string\n playlist: (playlistId: number) => string\n artist: (artistId: number) => string\n}\n\nexport const createDeepLink = (base: DeepLinkBase = \"open\"): DeepLink => {\n const root = `https://${base}.qobuz.com`\n return {\n album: (albumId) => `${root}/album/${albumId}`,\n track: (trackId) => `${root}/track/${trackId}`,\n playlist: (playlistId) => `${root}/playlist/${playlistId}`,\n artist: (artistId) => `${root}/artist/${artistId}`,\n }\n}\n","export type QobuzErrorKind = \"http\" | \"auth\" | \"bootstrap\"\n\nexport type QobuzError = Error & {\n kind: QobuzErrorKind\n status?: number\n}\n\nconst createError = (\n kind: QobuzErrorKind,\n message: string,\n status?: number,\n): QobuzError =>\n Object.assign(new Error(message), { name: \"QobuzError\", kind, status })\n\nexport const httpError = (message: string, status: number) =>\n createError(\"http\", message, status)\nexport const authError = (message: string) => createError(\"auth\", message)\nexport const bootstrapError = (message: string) =>\n createError(\"bootstrap\", message)\n","import { httpError } from \"./errors.js\"\n\nexport const QOBUZ_BASE_URL = \"https://www.qobuz.com/api.json/0.2\"\n\nexport const USER_AGENT =\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\"\n\nexport type QueryParams = Record<string, string | number | undefined>\n\nexport type Transport = {\n get: <T = unknown>(path: string, query?: QueryParams) => Promise<T>\n}\n\nexport type TransportConfig = {\n appId: string\n token?: string\n baseUrl?: string\n fetchImpl?: typeof fetch\n}\n\nconst toQuery = (params: QueryParams) =>\n new URLSearchParams(\n Object.entries(params)\n .filter(([, value]) => value !== undefined)\n .map(([key, value]) => [key, String(value)]),\n )\n\nexport const createTransport = ({\n appId,\n token,\n baseUrl = QOBUZ_BASE_URL,\n fetchImpl = fetch,\n}: TransportConfig): Transport => {\n const headers = {\n \"User-Agent\": USER_AGENT,\n \"X-App-Id\": appId,\n ...(token ? { \"X-User-Auth-Token\": token } : {}),\n }\n\n const get = async <T>(path: string, query: QueryParams = {}): Promise<T> => {\n const res = await fetchImpl(`${baseUrl}/${path}?${toQuery(query)}`, {\n headers,\n })\n if (!res.ok) {\n const body = await res.text().catch(() => \"\")\n throw httpError(\n `${path} failed (${res.status}): ${body.slice(0, 200)}`,\n res.status,\n )\n }\n return (await res.json()) as T\n }\n\n return { get }\n}\n","import type {\n Album,\n Artist,\n Playlist,\n QobuzImage,\n Track,\n} from \"./types/domain.js\"\n\ntype Raw = any\n\nconst mapImage = (raw: Raw | undefined): QobuzImage | undefined =>\n raw\n ? { thumbnail: raw.thumbnail, small: raw.small, large: raw.large }\n : undefined\n\nexport const mapArtist = (raw: Raw): Artist => ({\n id: raw.id,\n name: raw.name,\n picture: raw.picture ?? raw.image?.medium,\n albumsCount: raw.albums_count,\n})\n\nexport const mapAlbum = (raw: Raw): Album => ({\n id: String(raw.id),\n title: raw.title,\n artist: raw.artist ? mapArtist(raw.artist) : undefined,\n tracksCount: raw.tracks_count,\n releaseDate: raw.release_date_original ?? raw.released_at,\n duration: raw.duration,\n image: mapImage(raw.image),\n genre: raw.genre?.name,\n hires: raw.hires,\n})\n\nexport const mapTrack = (raw: Raw): Track => ({\n id: raw.id,\n title: raw.title,\n album: raw.album ? mapAlbum(raw.album) : undefined,\n artist: raw.performer\n ? mapArtist(raw.performer)\n : raw.artist\n ? mapArtist(raw.artist)\n : undefined,\n trackNumber: raw.track_number,\n duration: raw.duration,\n hires: raw.hires,\n})\n\nexport const mapPlaylist = (raw: Raw): Playlist => ({\n id: raw.id,\n name: raw.name,\n description: raw.description,\n tracksCount: raw.tracks_count,\n isPublic: raw.is_public,\n owner: raw.owner?.name,\n duration: raw.duration,\n})\n","import { mapAlbum } from \"../mappers.js\"\nimport type { Transport } from \"../http/transport.js\"\nimport type { Album } from \"../types/domain.js\"\n\nexport const createAlbumsResource = (transport: Transport) => ({\n get: async (albumId: string): Promise<Album> =>\n mapAlbum(await transport.get(\"album/get\", { album_id: albumId })),\n})\n","import { mapArtist } from \"../mappers.js\"\nimport type { Transport } from \"../http/transport.js\"\nimport type { Artist, PageOptions } from \"../types/domain.js\"\n\ntype RawSimilar = { artists?: { items?: unknown[] } }\n\nexport const createArtistsResource = (transport: Transport) => ({\n get: async (artistId: number): Promise<Artist> =>\n mapArtist(await transport.get(\"artist/get\", { artist_id: artistId })),\n getSimilar: async (\n artistId: number,\n options: PageOptions = {},\n ): Promise<Artist[]> => {\n const raw = await transport.get<RawSimilar>(\"artist/getSimilarArtists\", {\n artist_id: artistId,\n limit: options.limit ?? 20,\n offset: options.offset ?? 0,\n })\n return (raw.artists?.items ?? []).map(mapArtist)\n },\n})\n","import { mapAlbum, mapArtist, mapTrack } from \"../mappers.js\"\nimport type { Transport } from \"../http/transport.js\"\nimport type {\n Album,\n Artist,\n FavouriteType,\n PageOptions,\n Track,\n} from \"../types/domain.js\"\n\nexport type UserFavourites = {\n albums: Album[]\n artists: Artist[]\n tracks: Track[]\n}\n\ntype RawFavourites = {\n albums?: { items?: unknown[] }\n artists?: { items?: unknown[] }\n tracks?: { items?: unknown[] }\n}\n\nconst favouriteIdParam: Record<FavouriteType, string> = {\n albums: \"album_ids\",\n artists: \"artist_ids\",\n tracks: \"track_ids\",\n}\n\nexport const createFavouritesResource = (transport: Transport) => ({\n list: async (\n type: FavouriteType,\n options: PageOptions = {},\n ): Promise<UserFavourites> => {\n const raw = await transport.get<RawFavourites>(\n \"favorite/getUserFavorites\",\n {\n type,\n limit: options.limit ?? 50,\n offset: options.offset ?? 0,\n },\n )\n return {\n albums: (raw.albums?.items ?? []).map(mapAlbum),\n artists: (raw.artists?.items ?? []).map(mapArtist),\n tracks: (raw.tracks?.items ?? []).map(mapTrack),\n }\n },\n add: async (type: FavouriteType, id: string): Promise<void> => {\n await transport.get(\"favorite/create\", { [favouriteIdParam[type]]: id })\n },\n remove: async (type: FavouriteType, id: string): Promise<void> => {\n await transport.get(\"favorite/delete\", { [favouriteIdParam[type]]: id })\n },\n})\n","import { mapPlaylist } from \"../mappers.js\"\nimport type { Transport } from \"../http/transport.js\"\nimport type { PageOptions, Playlist } from \"../types/domain.js\"\n\ntype RawPlaylists = { playlists?: { items?: unknown[] } }\n\nexport type CreatePlaylistParams = {\n name: string\n description?: string\n isPublic?: boolean\n}\n\nexport const createPlaylistsResource = (transport: Transport) => ({\n listForUser: async (options: PageOptions = {}): Promise<Playlist[]> => {\n const raw = await transport.get<RawPlaylists>(\"playlist/getUserPlaylists\", {\n limit: options.limit ?? 50,\n offset: options.offset ?? 0,\n })\n return (raw.playlists?.items ?? []).map(mapPlaylist)\n },\n get: async (\n playlistId: number,\n options: PageOptions = {},\n ): Promise<Playlist> =>\n mapPlaylist(\n await transport.get(\"playlist/get\", {\n playlist_id: playlistId,\n extra: \"tracks\",\n limit: options.limit ?? 500,\n offset: options.offset ?? 0,\n }),\n ),\n create: async ({\n name,\n description,\n isPublic,\n }: CreatePlaylistParams): Promise<Playlist> =>\n mapPlaylist(\n await transport.get(\"playlist/create\", {\n name,\n description,\n is_public: isPublic ? 1 : 0,\n }),\n ),\n remove: async (playlistId: number): Promise<void> => {\n await transport.get(\"playlist/delete\", { playlist_id: playlistId })\n },\n addTracks: async (\n playlistId: number,\n trackIds: number[],\n ): Promise<Playlist> =>\n mapPlaylist(\n await transport.get(\"playlist/addTracks\", {\n playlist_id: playlistId,\n track_ids: trackIds.join(\",\"),\n }),\n ),\n removeTracks: async (\n playlistId: number,\n playlistTrackIds: number[],\n ): Promise<Playlist> =>\n mapPlaylist(\n await transport.get(\"playlist/deleteTracks\", {\n playlist_id: playlistId,\n playlist_track_ids: playlistTrackIds.join(\",\"),\n }),\n ),\n})\n","import { mapAlbum, mapArtist, mapTrack } from \"../mappers.js\"\nimport type { Transport } from \"../http/transport.js\"\nimport type { PageOptions, SearchResults } from \"../types/domain.js\"\n\ntype RawSearch = {\n albums?: { items?: unknown[] }\n tracks?: { items?: unknown[] }\n artists?: { items?: unknown[] }\n}\n\nexport const createSearchResource = (transport: Transport) => ({\n search: async (\n query: string,\n options: PageOptions = {},\n ): Promise<SearchResults> => {\n const raw = await transport.get<RawSearch>(\"catalog/search\", {\n query,\n limit: options.limit ?? 20,\n offset: options.offset ?? 0,\n })\n return {\n query,\n albums: (raw.albums?.items ?? []).map(mapAlbum),\n tracks: (raw.tracks?.items ?? []).map(mapTrack),\n artists: (raw.artists?.items ?? []).map(mapArtist),\n }\n },\n})\n","import { mapTrack } from \"../mappers.js\"\nimport type { Transport } from \"../http/transport.js\"\nimport type { Track } from \"../types/domain.js\"\n\nexport const createTracksResource = (transport: Transport) => ({\n get: async (trackId: number): Promise<Track> =>\n mapTrack(await transport.get(\"track/get\", { track_id: trackId })),\n})\n","import { createDeepLink } from \"./deep-link.js\"\nimport { authError } from \"./http/errors.js\"\nimport { createTransport } from \"./http/transport.js\"\nimport { createAlbumsResource } from \"./resources/albums.js\"\nimport { createArtistsResource } from \"./resources/artists.js\"\nimport { createFavouritesResource } from \"./resources/favourites.js\"\nimport { createPlaylistsResource } from \"./resources/playlists.js\"\nimport { createSearchResource } from \"./resources/search.js\"\nimport { createTracksResource } from \"./resources/tracks.js\"\nimport type { CredentialStore } from \"./auth/credential-store.js\"\n\nexport type QobuzClientConfig = {\n store: CredentialStore\n fetchImpl?: typeof fetch\n}\n\nexport const createQobuzClient = async ({\n store,\n fetchImpl,\n}: QobuzClientConfig) => {\n const credentials = await store.load()\n if (!credentials)\n throw authError(\"not connected — store a valid app_id + token first\")\n\n const transport = createTransport({\n appId: credentials.appId,\n token: credentials.token,\n fetchImpl,\n })\n\n return {\n appId: credentials.appId,\n search: createSearchResource(transport),\n albums: createAlbumsResource(transport),\n artists: createArtistsResource(transport),\n tracks: createTracksResource(transport),\n favourites: createFavouritesResource(transport),\n playlists: createPlaylistsResource(transport),\n deepLink: createDeepLink(),\n signOut: () => store.clear(),\n }\n}\n\nexport type QobuzClient = Awaited<ReturnType<typeof createQobuzClient>>\n","import { bootstrapError } from \"../http/errors.js\"\nimport { USER_AGENT } from \"../http/transport.js\"\n\nconst PLAY_URL = \"https://play.qobuz.com\"\n\nexport type AppCredentials = {\n appId: string\n bundlePath: string\n}\n\nexport const fetchAppId = async (\n options: { fetchImpl?: typeof fetch } = {},\n): Promise<AppCredentials> => {\n const fetchImpl = options.fetchImpl ?? fetch\n\n const getText = async (url: string) => {\n const res = await fetchImpl(url, { headers: { \"User-Agent\": USER_AGENT } })\n if (!res.ok) throw bootstrapError(`GET ${url} failed (${res.status})`)\n return res.text()\n }\n\n const loginPage = await getText(`${PLAY_URL}/login`)\n const bundlePath = loginPage.match(\n /<script src=\"(\\/resources\\/\\d+\\.\\d+\\.\\d+-[a-z]\\d{3}\\/bundle\\.js)\"><\\/script>/,\n )?.[1]\n if (!bundlePath)\n throw bootstrapError(\n \"could not find the bundle.js URL in the Qobuz login page\",\n )\n\n const bundle = await getText(`${PLAY_URL}${bundlePath}`)\n const appId = bundle.match(/production:\\{api:\\{appId:\"(\\d{9})\"/)?.[1]\n if (!appId)\n throw bootstrapError(\"could not extract app_id from the Qobuz web bundle\")\n\n return { appId, bundlePath }\n}\n","import { authError, type QobuzError } from \"../http/errors.js\"\nimport { createTransport } from \"../http/transport.js\"\n\nexport type ValidateConfig = {\n appId: string\n token: string\n fetchImpl?: typeof fetch\n}\n\nexport const validateCredentials = async ({\n appId,\n token,\n fetchImpl,\n}: ValidateConfig): Promise<void> => {\n const transport = createTransport({ appId, token, fetchImpl })\n try {\n await transport.get(\"favorite/getUserFavorites\", {\n type: \"albums\",\n limit: 1,\n })\n } catch (error) {\n const qobuzError = error as QobuzError\n if (qobuzError.status === 401) {\n throw authError(\n \"Qobuz rejected the credentials (401) — the token may be expired or the app_id doesn't match\",\n )\n }\n throw error\n }\n}\n","import { execFile } from \"node:child_process\"\nimport { promisify } from \"node:util\"\n\nconst exec = promisify(execFile)\n\nexport type StoredCredentials = {\n appId: string\n token: string\n savedAt?: string\n}\n\nexport type CredentialStore = {\n load: () => Promise<StoredCredentials | undefined>\n save: (credentials: StoredCredentials) => Promise<void>\n clear: () => Promise<void>\n}\n\nexport const createMemoryStore = (\n seed?: StoredCredentials,\n): CredentialStore => {\n let current = seed\n return {\n load: async () => current,\n save: async (credentials) => {\n current = credentials\n },\n clear: async () => {\n current = undefined\n },\n }\n}\n\nexport type KeychainStoreOptions = {\n service?: string\n account?: string\n}\n\nexport const createKeychainStore = (\n options: KeychainStoreOptions = {},\n): CredentialStore => {\n const service = options.service ?? \"qobuz\"\n const account = options.account ?? \"credentials\"\n\n return {\n load: async () => {\n try {\n const { stdout } = await exec(\"security\", [\n \"find-generic-password\",\n \"-s\",\n service,\n \"-a\",\n account,\n \"-w\",\n ])\n return JSON.parse(stdout.trim()) as StoredCredentials\n } catch {\n return undefined\n }\n },\n save: async (credentials) => {\n const value = JSON.stringify({\n ...credentials,\n savedAt: credentials.savedAt ?? new Date().toISOString(),\n })\n await exec(\"security\", [\n \"add-generic-password\",\n \"-U\",\n \"-s\",\n service,\n \"-a\",\n account,\n \"-w\",\n value,\n ])\n },\n clear: async () => {\n try {\n await exec(\"security\", [\n \"delete-generic-password\",\n \"-s\",\n service,\n \"-a\",\n account,\n ])\n } catch {\n // nothing stored — nothing to clear\n }\n },\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kud/qobuz",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reverse-engineered Qobuz API client for Node.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"lint": "eslint ."
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "26.0.0",
|
|
28
|
+
"tsup": "8.5.1",
|
|
29
|
+
"typescript": "6.0.3",
|
|
30
|
+
"vitest": "4.1.9"
|
|
31
|
+
}
|
|
32
|
+
}
|