@kud/qobuz 0.1.0 → 0.3.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 CHANGED
@@ -16,6 +16,7 @@
16
16
  - **Pluggable credential store** — ships in-memory and macOS Keychain implementations; bring your own.
17
17
  - **Typed resources** — search, albums, artists, tracks, playlists, favourites — clean camelCase domain types mapped from the raw API.
18
18
  - **Deep links** — build `open.qobuz.com` URLs to open anything in the Qobuz app.
19
+ - **Now playing** — read the track the Qobuz desktop app is currently playing (macOS), bypassing the OS now-playing system Qobuz never registers with.
19
20
  - **ESM + types, zero runtime deps** — tree-shakeable, fully typed, ships nothing extra.
20
21
 
21
22
  ## Install
@@ -26,17 +27,25 @@ npm install @kud/qobuz
26
27
 
27
28
  ## Usage
28
29
 
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
+ Grab a token from a logged-in [play.qobuz.com](https://play.qobuz.com) session — open DevTools, inspect any `api.json` network request, and copy the `X-User-Auth-Token` header. Then `connect` validates it, persists it to your store, and hands back a ready client (the `app_id` is scraped automatically if you don't pass one):
30
31
 
31
32
  ```ts
32
- import { createQobuzClient, createKeychainStore } from "@kud/qobuz"
33
+ import { connect, createKeychainStore } from "@kud/qobuz"
33
34
 
34
- const client = await createQobuzClient({ store: createKeychainStore() })
35
+ const client = await connect({ token, store: createKeychainStore() })
35
36
  const { albums } = await client.search.search("radiohead")
36
37
  console.log(client.deepLink.album(albums[0].id))
37
38
  ```
38
39
 
39
- The companion CLI [`@kud/qobuz-cli`](https://kud.io/projects/qobuz-cli) provides a `qobuz login` flow that handles credential storage automatically.
40
+ On later runs the token is already stored, so skip `connect` and build straight from the store:
41
+
42
+ ```ts
43
+ import { createQobuzClient, createKeychainStore } from "@kud/qobuz"
44
+
45
+ const client = await createQobuzClient({ store: createKeychainStore() })
46
+ ```
47
+
48
+ Prefer the terminal? The companion CLI [`@kud/qobuz-cli`](https://kud.io/projects/qobuz-cli) wraps all of this in a `qobuz login` flow.
40
49
 
41
50
  ## Development
42
51
 
package/dist/index.d.ts CHANGED
@@ -84,6 +84,21 @@ type UserFavourites = {
84
84
  tracks: Track[];
85
85
  };
86
86
 
87
+ type NowPlayingOptions = {
88
+ /** Override the Qobuz desktop player-state file (defaults to the macOS path). */
89
+ path?: string;
90
+ };
91
+ /**
92
+ * The Qobuz desktop app (Electron) persists its play queue here. macOS does not
93
+ * expose Qobuz to its now-playing system, so this local file is the source of truth.
94
+ */
95
+ declare const defaultPlayerStatePath: () => string;
96
+ /**
97
+ * The id of the track Qobuz is currently playing, read from the desktop app's
98
+ * local state. Returns `undefined` if nothing is playing or the file is absent.
99
+ */
100
+ declare const readNowPlayingTrackId: (options?: NowPlayingOptions) => Promise<number | undefined>;
101
+
87
102
  type StoredCredentials = {
88
103
  appId: string;
89
104
  token: string;
@@ -134,6 +149,7 @@ declare const createQobuzClient: ({ store, fetchImpl, }: QobuzClientConfig) => P
134
149
  removeTracks: (playlistId: number, playlistTrackIds: number[]) => Promise<Playlist>;
135
150
  };
136
151
  deepLink: DeepLink;
152
+ nowPlaying: (options?: NowPlayingOptions) => Promise<Track | undefined>;
137
153
  signOut: () => Promise<void>;
138
154
  }>;
139
155
  type QobuzClient = Awaited<ReturnType<typeof createQobuzClient>>;
@@ -153,10 +169,18 @@ type ValidateConfig = {
153
169
  };
154
170
  declare const validateCredentials: ({ appId, token, fetchImpl, }: ValidateConfig) => Promise<void>;
155
171
 
172
+ type ConnectConfig = {
173
+ token: string;
174
+ appId?: string;
175
+ store: CredentialStore;
176
+ fetchImpl?: typeof fetch;
177
+ };
178
+ declare const connect: ({ token, appId, store, fetchImpl, }: ConnectConfig) => Promise<QobuzClient>;
179
+
156
180
  type QobuzErrorKind = "http" | "auth" | "bootstrap";
157
181
  type QobuzError = Error & {
158
182
  kind: QobuzErrorKind;
159
183
  status?: number;
160
184
  };
161
185
 
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 };
186
+ export { type Album, type AppCredentials, type Artist, type ConnectConfig, type CreatePlaylistParams, type CredentialStore, type DeepLink, type DeepLinkBase, type FavouriteType, type KeychainStoreOptions, type NowPlayingOptions, 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, connect, createDeepLink, createKeychainStore, createMemoryStore, createQobuzClient, createTransport, defaultPlayerStatePath, fetchAppId, readNowPlayingTrackId, validateCredentials };
package/dist/index.js CHANGED
@@ -201,6 +201,24 @@ var createTracksResource = (transport) => ({
201
201
  get: async (trackId) => mapTrack(await transport.get("track/get", { track_id: trackId }))
202
202
  });
203
203
 
204
+ // src/now-playing.ts
205
+ import { readFile } from "fs/promises";
206
+ import { homedir } from "os";
207
+ import { join } from "path";
208
+ var defaultPlayerStatePath = () => join(homedir(), "Library/Application Support/Qobuz/player-0.json");
209
+ var readNowPlayingTrackId = async (options = {}) => {
210
+ try {
211
+ const state = JSON.parse(
212
+ await readFile(options.path ?? defaultPlayerStatePath(), "utf8")
213
+ );
214
+ const queue = state?.playqueue?.data;
215
+ const trackId = queue?.items?.[queue?.currentIndex]?.trackId;
216
+ return typeof trackId === "number" ? trackId : void 0;
217
+ } catch {
218
+ return void 0;
219
+ }
220
+ };
221
+
204
222
  // src/client.ts
205
223
  var createQobuzClient = async ({
206
224
  store,
@@ -214,15 +232,20 @@ var createQobuzClient = async ({
214
232
  token: credentials.token,
215
233
  fetchImpl
216
234
  });
235
+ const tracks = createTracksResource(transport);
217
236
  return {
218
237
  appId: credentials.appId,
219
238
  search: createSearchResource(transport),
220
239
  albums: createAlbumsResource(transport),
221
240
  artists: createArtistsResource(transport),
222
- tracks: createTracksResource(transport),
241
+ tracks,
223
242
  favourites: createFavouritesResource(transport),
224
243
  playlists: createPlaylistsResource(transport),
225
244
  deepLink: createDeepLink(),
245
+ nowPlaying: async (options) => {
246
+ const trackId = await readNowPlayingTrackId(options);
247
+ return trackId === void 0 ? void 0 : tracks.get(trackId);
248
+ },
226
249
  signOut: () => store.clear()
227
250
  };
228
251
  };
@@ -274,6 +297,19 @@ var validateCredentials = async ({
274
297
  }
275
298
  };
276
299
 
300
+ // src/auth/connect.ts
301
+ var connect = async ({
302
+ token,
303
+ appId,
304
+ store,
305
+ fetchImpl
306
+ }) => {
307
+ const resolvedAppId = appId ?? (await fetchAppId({ fetchImpl })).appId;
308
+ await validateCredentials({ appId: resolvedAppId, token, fetchImpl });
309
+ await store.save({ appId: resolvedAppId, token });
310
+ return createQobuzClient({ store, fetchImpl });
311
+ };
312
+
277
313
  // src/auth/credential-store.ts
278
314
  import { execFile } from "child_process";
279
315
  import { promisify } from "util";
@@ -341,12 +377,15 @@ var createKeychainStore = (options = {}) => {
341
377
  };
342
378
  export {
343
379
  QOBUZ_BASE_URL,
380
+ connect,
344
381
  createDeepLink,
345
382
  createKeychainStore,
346
383
  createMemoryStore,
347
384
  createQobuzClient,
348
385
  createTransport,
386
+ defaultPlayerStatePath,
349
387
  fetchAppId,
388
+ readNowPlayingTrackId,
350
389
  validateCredentials
351
390
  };
352
391
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +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":[]}
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/now-playing.ts","../src/client.ts","../src/auth/bootstrap.ts","../src/auth/validate.ts","../src/auth/connect.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 { readFile } from \"node:fs/promises\"\nimport { homedir } from \"node:os\"\nimport { join } from \"node:path\"\n\nexport type NowPlayingOptions = {\n /** Override the Qobuz desktop player-state file (defaults to the macOS path). */\n path?: string\n}\n\n/**\n * The Qobuz desktop app (Electron) persists its play queue here. macOS does not\n * expose Qobuz to its now-playing system, so this local file is the source of truth.\n */\nexport const defaultPlayerStatePath = (): string =>\n join(homedir(), \"Library/Application Support/Qobuz/player-0.json\")\n\n/**\n * The id of the track Qobuz is currently playing, read from the desktop app's\n * local state. Returns `undefined` if nothing is playing or the file is absent.\n */\nexport const readNowPlayingTrackId = async (\n options: NowPlayingOptions = {},\n): Promise<number | undefined> => {\n try {\n const state = JSON.parse(\n await readFile(options.path ?? defaultPlayerStatePath(), \"utf8\"),\n )\n const queue = state?.playqueue?.data\n const trackId = queue?.items?.[queue?.currentIndex]?.trackId\n return typeof trackId === \"number\" ? trackId : undefined\n } catch {\n return undefined\n }\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 { readNowPlayingTrackId, type NowPlayingOptions } from \"./now-playing.js\"\nimport type { CredentialStore } from \"./auth/credential-store.js\"\nimport type { Track } from \"./types/domain.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 const tracks = createTracksResource(transport)\n\n return {\n appId: credentials.appId,\n search: createSearchResource(transport),\n albums: createAlbumsResource(transport),\n artists: createArtistsResource(transport),\n tracks,\n favourites: createFavouritesResource(transport),\n playlists: createPlaylistsResource(transport),\n deepLink: createDeepLink(),\n nowPlaying: async (\n options?: NowPlayingOptions,\n ): Promise<Track | undefined> => {\n const trackId = await readNowPlayingTrackId(options)\n return trackId === undefined ? undefined : tracks.get(trackId)\n },\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 { createQobuzClient, type QobuzClient } from \"../client.js\"\nimport { fetchAppId } from \"./bootstrap.js\"\nimport { validateCredentials } from \"./validate.js\"\nimport type { CredentialStore } from \"./credential-store.js\"\n\nexport type ConnectConfig = {\n token: string\n appId?: string\n store: CredentialStore\n fetchImpl?: typeof fetch\n}\n\nexport const connect = async ({\n token,\n appId,\n store,\n fetchImpl,\n}: ConnectConfig): Promise<QobuzClient> => {\n const resolvedAppId = appId ?? (await fetchAppId({ fetchImpl })).appId\n await validateCredentials({ appId: resolvedAppId, token, fetchImpl })\n await store.save({ appId: resolvedAppId, token })\n return createQobuzClient({ store, fetchImpl })\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 CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@kud/qobuz",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Reverse-engineered Qobuz API client for Node.js",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/kud/qobuz.git"
10
+ },
7
11
  "exports": {
8
12
  ".": {
9
13
  "types": "./dist/index.d.ts",