@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 +12 -4
- package/dist/index.d.ts +9 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,17 +26,25 @@ npm install @kud/qobuz
|
|
|
26
26
|
|
|
27
27
|
## Usage
|
|
28
28
|
|
|
29
|
-
Grab
|
|
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 {
|
|
32
|
+
import { connect, createKeychainStore } from "@kud/qobuz"
|
|
33
33
|
|
|
34
|
-
const client = await
|
|
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
|
-
|
|
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":";AASO,IAAM,iBAAiB,CAAC,OAAqB,WAAqB;AACvE,QAAM,OAAO,WAAW,IAAI;AAC5B,SAAO;AAAA,IACL,OAAO,CAAC,YAAY,GAAG,IAAI,UAAU,OAAO;AAAA,IAC5C,OAAO,CAAC,YAAY,GAAG,IAAI,UAAU,OAAO;AAAA,IAC5C,UAAU,CAAC,eAAe,GAAG,IAAI,aAAa,UAAU;AAAA,IACxD,QAAQ,CAAC,aAAa,GAAG,IAAI,WAAW,QAAQ;AAAA,EAClD;AACF;;;ACVA,IAAM,cAAc,CAClB,MACA,SACA,WAEA,OAAO,OAAO,IAAI,MAAM,OAAO,GAAG,EAAE,MAAM,cAAc,MAAM,OAAO,CAAC;AAEjE,IAAM,YAAY,CAAC,SAAiB,WACzC,YAAY,QAAQ,SAAS,MAAM;AAC9B,IAAM,YAAY,CAAC,YAAoB,YAAY,QAAQ,OAAO;AAClE,IAAM,iBAAiB,CAAC,YAC7B,YAAY,aAAa,OAAO;;;AChB3B,IAAM,iBAAiB;AAEvB,IAAM,aACX;AAeF,IAAM,UAAU,CAAC,WACf,IAAI;AAAA,EACF,OAAO,QAAQ,MAAM,EAClB,OAAO,CAAC,CAAC,EAAE,KAAK,MAAM,UAAU,MAAS,EACzC,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,OAAO,KAAK,CAAC,CAAC;AAC/C;AAEK,IAAM,kBAAkB,CAAC;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,YAAY;AACd,MAAkC;AAChC,QAAM,UAAU;AAAA,IACd,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,GAAI,QAAQ,EAAE,qBAAqB,MAAM,IAAI,CAAC;AAAA,EAChD;AAEA,QAAM,MAAM,OAAU,MAAc,QAAqB,CAAC,MAAkB;AAC1E,UAAM,MAAM,MAAM,UAAU,GAAG,OAAO,IAAI,IAAI,IAAI,QAAQ,KAAK,CAAC,IAAI;AAAA,MAClE;AAAA,IACF,CAAC;AACD,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,YAAM;AAAA,QACJ,GAAG,IAAI,YAAY,IAAI,MAAM,MAAM,KAAK,MAAM,GAAG,GAAG,CAAC;AAAA,QACrD,IAAI;AAAA,MACN;AAAA,IACF;AACA,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAEA,SAAO,EAAE,IAAI;AACf;;;AC5CA,IAAM,WAAW,CAAC,QAChB,MACI,EAAE,WAAW,IAAI,WAAW,OAAO,IAAI,OAAO,OAAO,IAAI,MAAM,IAC/D;AAEC,IAAM,YAAY,CAAC,SAAsB;AAAA,EAC9C,IAAI,IAAI;AAAA,EACR,MAAM,IAAI;AAAA,EACV,SAAS,IAAI,WAAW,IAAI,OAAO;AAAA,EACnC,aAAa,IAAI;AACnB;AAEO,IAAM,WAAW,CAAC,SAAqB;AAAA,EAC5C,IAAI,OAAO,IAAI,EAAE;AAAA,EACjB,OAAO,IAAI;AAAA,EACX,QAAQ,IAAI,SAAS,UAAU,IAAI,MAAM,IAAI;AAAA,EAC7C,aAAa,IAAI;AAAA,EACjB,aAAa,IAAI,yBAAyB,IAAI;AAAA,EAC9C,UAAU,IAAI;AAAA,EACd,OAAO,SAAS,IAAI,KAAK;AAAA,EACzB,OAAO,IAAI,OAAO;AAAA,EAClB,OAAO,IAAI;AACb;AAEO,IAAM,WAAW,CAAC,SAAqB;AAAA,EAC5C,IAAI,IAAI;AAAA,EACR,OAAO,IAAI;AAAA,EACX,OAAO,IAAI,QAAQ,SAAS,IAAI,KAAK,IAAI;AAAA,EACzC,QAAQ,IAAI,YACR,UAAU,IAAI,SAAS,IACvB,IAAI,SACF,UAAU,IAAI,MAAM,IACpB;AAAA,EACN,aAAa,IAAI;AAAA,EACjB,UAAU,IAAI;AAAA,EACd,OAAO,IAAI;AACb;AAEO,IAAM,cAAc,CAAC,SAAwB;AAAA,EAClD,IAAI,IAAI;AAAA,EACR,MAAM,IAAI;AAAA,EACV,aAAa,IAAI;AAAA,EACjB,aAAa,IAAI;AAAA,EACjB,UAAU,IAAI;AAAA,EACd,OAAO,IAAI,OAAO;AAAA,EAClB,UAAU,IAAI;AAChB;;;ACpDO,IAAM,uBAAuB,CAAC,eAA0B;AAAA,EAC7D,KAAK,OAAO,YACV,SAAS,MAAM,UAAU,IAAI,aAAa,EAAE,UAAU,QAAQ,CAAC,CAAC;AACpE;;;ACDO,IAAM,wBAAwB,CAAC,eAA0B;AAAA,EAC9D,KAAK,OAAO,aACV,UAAU,MAAM,UAAU,IAAI,cAAc,EAAE,WAAW,SAAS,CAAC,CAAC;AAAA,EACtE,YAAY,OACV,UACA,UAAuB,CAAC,MACF;AACtB,UAAM,MAAM,MAAM,UAAU,IAAgB,4BAA4B;AAAA,MACtE,WAAW;AAAA,MACX,OAAO,QAAQ,SAAS;AAAA,MACxB,QAAQ,QAAQ,UAAU;AAAA,IAC5B,CAAC;AACD,YAAQ,IAAI,SAAS,SAAS,CAAC,GAAG,IAAI,SAAS;AAAA,EACjD;AACF;;;ACEA,IAAM,mBAAkD;AAAA,EACtD,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AACV;AAEO,IAAM,2BAA2B,CAAC,eAA0B;AAAA,EACjE,MAAM,OACJ,MACA,UAAuB,CAAC,MACI;AAC5B,UAAM,MAAM,MAAM,UAAU;AAAA,MAC1B;AAAA,MACA;AAAA,QACE;AAAA,QACA,OAAO,QAAQ,SAAS;AAAA,QACxB,QAAQ,QAAQ,UAAU;AAAA,MAC5B;AAAA,IACF;AACA,WAAO;AAAA,MACL,SAAS,IAAI,QAAQ,SAAS,CAAC,GAAG,IAAI,QAAQ;AAAA,MAC9C,UAAU,IAAI,SAAS,SAAS,CAAC,GAAG,IAAI,SAAS;AAAA,MACjD,SAAS,IAAI,QAAQ,SAAS,CAAC,GAAG,IAAI,QAAQ;AAAA,IAChD;AAAA,EACF;AAAA,EACA,KAAK,OAAO,MAAqB,OAA8B;AAC7D,UAAM,UAAU,IAAI,mBAAmB,EAAE,CAAC,iBAAiB,IAAI,CAAC,GAAG,GAAG,CAAC;AAAA,EACzE;AAAA,EACA,QAAQ,OAAO,MAAqB,OAA8B;AAChE,UAAM,UAAU,IAAI,mBAAmB,EAAE,CAAC,iBAAiB,IAAI,CAAC,GAAG,GAAG,CAAC;AAAA,EACzE;AACF;;;ACzCO,IAAM,0BAA0B,CAAC,eAA0B;AAAA,EAChE,aAAa,OAAO,UAAuB,CAAC,MAA2B;AACrE,UAAM,MAAM,MAAM,UAAU,IAAkB,6BAA6B;AAAA,MACzE,OAAO,QAAQ,SAAS;AAAA,MACxB,QAAQ,QAAQ,UAAU;AAAA,IAC5B,CAAC;AACD,YAAQ,IAAI,WAAW,SAAS,CAAC,GAAG,IAAI,WAAW;AAAA,EACrD;AAAA,EACA,KAAK,OACH,YACA,UAAuB,CAAC,MAExB;AAAA,IACE,MAAM,UAAU,IAAI,gBAAgB;AAAA,MAClC,aAAa;AAAA,MACb,OAAO;AAAA,MACP,OAAO,QAAQ,SAAS;AAAA,MACxB,QAAQ,QAAQ,UAAU;AAAA,IAC5B,CAAC;AAAA,EACH;AAAA,EACF,QAAQ,OAAO;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,EACF,MACE;AAAA,IACE,MAAM,UAAU,IAAI,mBAAmB;AAAA,MACrC;AAAA,MACA;AAAA,MACA,WAAW,WAAW,IAAI;AAAA,IAC5B,CAAC;AAAA,EACH;AAAA,EACF,QAAQ,OAAO,eAAsC;AACnD,UAAM,UAAU,IAAI,mBAAmB,EAAE,aAAa,WAAW,CAAC;AAAA,EACpE;AAAA,EACA,WAAW,OACT,YACA,aAEA;AAAA,IACE,MAAM,UAAU,IAAI,sBAAsB;AAAA,MACxC,aAAa;AAAA,MACb,WAAW,SAAS,KAAK,GAAG;AAAA,IAC9B,CAAC;AAAA,EACH;AAAA,EACF,cAAc,OACZ,YACA,qBAEA;AAAA,IACE,MAAM,UAAU,IAAI,yBAAyB;AAAA,MAC3C,aAAa;AAAA,MACb,oBAAoB,iBAAiB,KAAK,GAAG;AAAA,IAC/C,CAAC;AAAA,EACH;AACJ;;;ACzDO,IAAM,uBAAuB,CAAC,eAA0B;AAAA,EAC7D,QAAQ,OACN,OACA,UAAuB,CAAC,MACG;AAC3B,UAAM,MAAM,MAAM,UAAU,IAAe,kBAAkB;AAAA,MAC3D;AAAA,MACA,OAAO,QAAQ,SAAS;AAAA,MACxB,QAAQ,QAAQ,UAAU;AAAA,IAC5B,CAAC;AACD,WAAO;AAAA,MACL;AAAA,MACA,SAAS,IAAI,QAAQ,SAAS,CAAC,GAAG,IAAI,QAAQ;AAAA,MAC9C,SAAS,IAAI,QAAQ,SAAS,CAAC,GAAG,IAAI,QAAQ;AAAA,MAC9C,UAAU,IAAI,SAAS,SAAS,CAAC,GAAG,IAAI,SAAS;AAAA,IACnD;AAAA,EACF;AACF;;;ACvBO,IAAM,uBAAuB,CAAC,eAA0B;AAAA,EAC7D,KAAK,OAAO,YACV,SAAS,MAAM,UAAU,IAAI,aAAa,EAAE,UAAU,QAAQ,CAAC,CAAC;AACpE;;;ACSO,IAAM,oBAAoB,OAAO;AAAA,EACtC;AAAA,EACA;AACF,MAAyB;AACvB,QAAM,cAAc,MAAM,MAAM,KAAK;AACrC,MAAI,CAAC;AACH,UAAM,UAAU,yDAAoD;AAEtE,QAAM,YAAY,gBAAgB;AAAA,IAChC,OAAO,YAAY;AAAA,IACnB,OAAO,YAAY;AAAA,IACnB;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL,OAAO,YAAY;AAAA,IACnB,QAAQ,qBAAqB,SAAS;AAAA,IACtC,QAAQ,qBAAqB,SAAS;AAAA,IACtC,SAAS,sBAAsB,SAAS;AAAA,IACxC,QAAQ,qBAAqB,SAAS;AAAA,IACtC,YAAY,yBAAyB,SAAS;AAAA,IAC9C,WAAW,wBAAwB,SAAS;AAAA,IAC5C,UAAU,eAAe;AAAA,IACzB,SAAS,MAAM,MAAM,MAAM;AAAA,EAC7B;AACF;;;ACtCA,IAAM,WAAW;AAOV,IAAM,aAAa,OACxB,UAAwC,CAAC,MACb;AAC5B,QAAM,YAAY,QAAQ,aAAa;AAEvC,QAAM,UAAU,OAAO,QAAgB;AACrC,UAAM,MAAM,MAAM,UAAU,KAAK,EAAE,SAAS,EAAE,cAAc,WAAW,EAAE,CAAC;AAC1E,QAAI,CAAC,IAAI,GAAI,OAAM,eAAe,OAAO,GAAG,YAAY,IAAI,MAAM,GAAG;AACrE,WAAO,IAAI,KAAK;AAAA,EAClB;AAEA,QAAM,YAAY,MAAM,QAAQ,GAAG,QAAQ,QAAQ;AACnD,QAAM,aAAa,UAAU;AAAA,IAC3B;AAAA,EACF,IAAI,CAAC;AACL,MAAI,CAAC;AACH,UAAM;AAAA,MACJ;AAAA,IACF;AAEF,QAAM,SAAS,MAAM,QAAQ,GAAG,QAAQ,GAAG,UAAU,EAAE;AACvD,QAAM,QAAQ,OAAO,MAAM,oCAAoC,IAAI,CAAC;AACpE,MAAI,CAAC;AACH,UAAM,eAAe,oDAAoD;AAE3E,SAAO,EAAE,OAAO,WAAW;AAC7B;;;AC3BO,IAAM,sBAAsB,OAAO;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AACF,MAAqC;AACnC,QAAM,YAAY,gBAAgB,EAAE,OAAO,OAAO,UAAU,CAAC;AAC7D,MAAI;AACF,UAAM,UAAU,IAAI,6BAA6B;AAAA,MAC/C,MAAM;AAAA,MACN,OAAO;AAAA,IACT,CAAC;AAAA,EACH,SAAS,OAAO;AACd,UAAM,aAAa;AACnB,QAAI,WAAW,WAAW,KAAK;AAC7B,YAAM;AAAA,QACJ;AAAA,MACF;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;;;ACjBO,IAAM,UAAU,OAAO;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAA2C;AACzC,QAAM,gBAAgB,UAAU,MAAM,WAAW,EAAE,UAAU,CAAC,GAAG;AACjE,QAAM,oBAAoB,EAAE,OAAO,eAAe,OAAO,UAAU,CAAC;AACpE,QAAM,MAAM,KAAK,EAAE,OAAO,eAAe,MAAM,CAAC;AAChD,SAAO,kBAAkB,EAAE,OAAO,UAAU,CAAC;AAC/C;;;ACtBA,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAE1B,IAAM,OAAO,UAAU,QAAQ;AAcxB,IAAM,oBAAoB,CAC/B,SACoB;AACpB,MAAI,UAAU;AACd,SAAO;AAAA,IACL,MAAM,YAAY;AAAA,IAClB,MAAM,OAAO,gBAAgB;AAC3B,gBAAU;AAAA,IACZ;AAAA,IACA,OAAO,YAAY;AACjB,gBAAU;AAAA,IACZ;AAAA,EACF;AACF;AAOO,IAAM,sBAAsB,CACjC,UAAgC,CAAC,MACb;AACpB,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,UAAU,QAAQ,WAAW;AAEnC,SAAO;AAAA,IACL,MAAM,YAAY;AAChB,UAAI;AACF,cAAM,EAAE,OAAO,IAAI,MAAM,KAAK,YAAY;AAAA,UACxC;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AACD,eAAO,KAAK,MAAM,OAAO,KAAK,CAAC;AAAA,MACjC,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,MAAM,OAAO,gBAAgB;AAC3B,YAAM,QAAQ,KAAK,UAAU;AAAA,QAC3B,GAAG;AAAA,QACH,SAAS,YAAY,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MACzD,CAAC;AACD,YAAM,KAAK,YAAY;AAAA,QACrB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,OAAO,YAAY;AACjB,UAAI;AACF,cAAM,KAAK,YAAY;AAAA,UACrB;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|