@kud/qobuz 0.1.0 → 0.2.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
@@ -26,17 +26,25 @@ npm install @kud/qobuz
26
26
 
27
27
  ## Usage
28
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:
29
+ 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
30
 
31
31
  ```ts
32
- import { createQobuzClient, createKeychainStore } from "@kud/qobuz"
32
+ import { connect, createKeychainStore } from "@kud/qobuz"
33
33
 
34
- const client = await createQobuzClient({ store: createKeychainStore() })
34
+ const client = await connect({ token, store: createKeychainStore() })
35
35
  const { albums } = await client.search.search("radiohead")
36
36
  console.log(client.deepLink.album(albums[0].id))
37
37
  ```
38
38
 
39
- The companion CLI [`@kud/qobuz-cli`](https://kud.io/projects/qobuz-cli) provides a `qobuz login` flow that handles credential storage automatically.
39
+ On later runs the token is already stored, so skip `connect` and build straight from the store:
40
+
41
+ ```ts
42
+ import { createQobuzClient, createKeychainStore } from "@kud/qobuz"
43
+
44
+ const client = await createQobuzClient({ store: createKeychainStore() })
45
+ ```
46
+
47
+ 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
48
 
41
49
  ## Development
42
50
 
package/dist/index.d.ts CHANGED
@@ -153,10 +153,18 @@ type ValidateConfig = {
153
153
  };
154
154
  declare const validateCredentials: ({ appId, token, fetchImpl, }: ValidateConfig) => Promise<void>;
155
155
 
156
+ type ConnectConfig = {
157
+ token: string;
158
+ appId?: string;
159
+ store: CredentialStore;
160
+ fetchImpl?: typeof fetch;
161
+ };
162
+ declare const connect: ({ token, appId, store, fetchImpl, }: ConnectConfig) => Promise<QobuzClient>;
163
+
156
164
  type QobuzErrorKind = "http" | "auth" | "bootstrap";
157
165
  type QobuzError = Error & {
158
166
  kind: QobuzErrorKind;
159
167
  status?: number;
160
168
  };
161
169
 
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 };
170
+ export { type Album, type AppCredentials, type Artist, type ConnectConfig, 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, connect, createDeepLink, createKeychainStore, createMemoryStore, createQobuzClient, createTransport, fetchAppId, validateCredentials };
package/dist/index.js CHANGED
@@ -274,6 +274,19 @@ var validateCredentials = async ({
274
274
  }
275
275
  };
276
276
 
277
+ // src/auth/connect.ts
278
+ var connect = async ({
279
+ token,
280
+ appId,
281
+ store,
282
+ fetchImpl
283
+ }) => {
284
+ const resolvedAppId = appId ?? (await fetchAppId({ fetchImpl })).appId;
285
+ await validateCredentials({ appId: resolvedAppId, token, fetchImpl });
286
+ await store.save({ appId: resolvedAppId, token });
287
+ return createQobuzClient({ store, fetchImpl });
288
+ };
289
+
277
290
  // src/auth/credential-store.ts
278
291
  import { execFile } from "child_process";
279
292
  import { promisify } from "util";
@@ -341,6 +354,7 @@ var createKeychainStore = (options = {}) => {
341
354
  };
342
355
  export {
343
356
  QOBUZ_BASE_URL,
357
+ connect,
344
358
  createDeepLink,
345
359
  createKeychainStore,
346
360
  createMemoryStore,
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/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 { 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 { 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@kud/qobuz",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Reverse-engineered Qobuz API client for Node.js",
5
5
  "type": "module",
6
6
  "license": "MIT",