yoto-nodejs-client 0.0.9 → 0.0.11

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/bin/auth.js CHANGED
@@ -108,7 +108,7 @@ async function main () {
108
108
  if (error.message === 'Device code has expired') {
109
109
  console.error('\n❌ Device code has expired. Please run the command again.')
110
110
  process.exit(1)
111
- } else if (error.body?.error === 'expired_token') {
111
+ } else if (error.jsonBody?.error === 'expired_token') {
112
112
  console.error('\n❌ Device code has expired. Please run the command again.')
113
113
  process.exit(1)
114
114
  } else {
@@ -230,6 +230,7 @@ async function main () {
230
230
  }
231
231
 
232
232
  if (showFullState) {
233
+ const cardDuration = playback.cardDurationSeconds === null ? null : `${playback.cardDurationSeconds}s`
233
234
  // Show full playback state
234
235
  console.log(`\n${playbackIcon} PLAYBACK UPDATE [${timestamp}]: (FULL STATE)`)
235
236
  console.log(` Status: ${playback.playbackStatus}`)
@@ -239,6 +240,12 @@ async function main () {
239
240
  console.log(` Chapter Key: ${playback.chapterKey}`)
240
241
  console.log(` Position: ${playback.position}/${playback.trackLength}s`)
241
242
  console.log(` Card ID: ${playback.cardId}`)
243
+ console.log(` Card Title: ${playback.cardTitle}`)
244
+ console.log(` Card Slug: ${playback.cardSlug}`)
245
+ console.log(` Card Cover Image: ${playback.cardCoverImageUrl}`)
246
+ console.log(` Card Author: ${playback.cardAuthor}`)
247
+ console.log(` Card Read By: ${playback.cardReadBy}`)
248
+ console.log(` Card Duration: ${cardDuration}`)
242
249
  console.log(` Source: ${playback.source}`)
243
250
  console.log(` Streaming: ${playback.streaming}`)
244
251
  console.log(` Sleep Timer Active: ${playback.sleepTimerActive}`)
@@ -249,16 +256,22 @@ async function main () {
249
256
  // Show only changed fields
250
257
  console.log(`\n${playbackIcon} PLAYBACK UPDATE [${timestamp}]: (${changedFields.size} change${changedFields.size === 1 ? '' : 's'})`)
251
258
  for (const field of changedFields) {
259
+ if (field === 'trackLength' && changedFields.has('position')) {
260
+ continue
261
+ }
262
+
252
263
  let value = playback[field]
253
264
  if (field === 'position' || field === 'trackLength') {
254
- // Show position/trackLength together if either changed
265
+ // Show position/trackLength together if either changed.
255
266
  value = `${playback.position}/${playback.trackLength}s`
256
267
  console.log(` position/trackLength: ${value}`)
257
- // Skip if we already printed this combo
258
- if (field === 'trackLength') continue
259
- } else {
260
- console.log(` ${field}: ${value}`)
268
+ continue
269
+ }
270
+
271
+ if (field === 'cardDurationSeconds' && typeof value === 'number') {
272
+ value = `${value}s`
261
273
  }
274
+ console.log(` ${field}: ${value}`)
262
275
  }
263
276
  } else {
264
277
  console.log(`\n${playbackIcon} PLAYBACK UPDATE [${timestamp}]: (no changes)`)
@@ -1 +1 @@
1
- {"version":3,"file":"cli-helpers.d.ts","sourceRoot":"","sources":["cli-helpers.js"],"names":[],"mappings":"AAQA;;;GAGG;AACH,oCAFa,gCAAgC,CAoB5C;AAED;;;;GAIG;AACH,sCAHW,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;GAKG;AACH,wCAJW;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAA;CAAE,GACtD;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAoB5F;AAED;;;;;;;;GAQG;AACH,sFANG;IAAwB,QAAQ,EAAxB,MAAM;IACU,YAAY,EAA5B,MAAM;IACU,WAAW,EAA3B,MAAM;IACW,UAAU;CACnC,GAAU,UAAU,CAwBtB;AAED;;;GAGG;AACH,sCAFW,GAAG,QAyBb;AAED;;;GAGG;AACH,mCAFW,MAAM,QAKhB;sDA5IoD,YAAY;2BAGtC,yBAAyB"}
1
+ {"version":3,"file":"cli-helpers.d.ts","sourceRoot":"","sources":["cli-helpers.js"],"names":[],"mappings":"AAQA;;;GAGG;AACH,oCAFa,gCAAgC,CAoB5C;AAED;;;;GAIG;AACH,sCAHW,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;GAKG;AACH,wCAJW;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAA;CAAE,GACtD;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAoB5F;AAED;;;;;;;;GAQG;AACH,sFANG;IAAwB,QAAQ,EAAxB,MAAM;IACU,YAAY,EAA5B,MAAM;IACU,WAAW,EAA3B,MAAM;IACW,UAAU;CACnC,GAAU,UAAU,CAwBtB;AAED;;;GAGG;AACH,sCAFW,GAAG,QA4Bb;AAED;;;GAGG;AACH,mCAFW,MAAM,QAKhB;sDA/IoD,YAAY;2BAGtC,yBAAyB"}
@@ -116,17 +116,20 @@ export function handleCliError (error) {
116
116
  console.error(`Status Code: ${error.statusCode}`)
117
117
  }
118
118
 
119
- if (error.body?.error) {
120
- console.error(`Error: ${error.body.error}`)
119
+ if (error.jsonBody?.error) {
120
+ console.error(`Error: ${error.jsonBody.error}`)
121
121
  }
122
122
 
123
- if (error.body?.error_description) {
124
- console.error(`Description: ${error.body.error_description}`)
123
+ if (error.jsonBody?.error_description) {
124
+ console.error(`Description: ${error.jsonBody.error_description}`)
125
125
  }
126
126
 
127
- if (error.body && typeof error.body === 'object') {
127
+ if (error.jsonBody && typeof error.jsonBody === 'object') {
128
128
  console.error('\nFull error response:')
129
- console.error(JSON.stringify(error.body, null, 2))
129
+ console.error(JSON.stringify(error.jsonBody, null, 2))
130
+ } else if (error.textBody) {
131
+ console.error('\nRaw error response:')
132
+ console.error(error.textBody)
130
133
  }
131
134
 
132
135
  process.exit(1)
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["auth.js"],"names":[],"mappings":"AAWA;;;GAGG;AAEH;;;GAGG;AAEH;;;GAGG;AAEH;;;GAGG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,4JAbG;IAA0B,QAAQ;IACR,KAAK;IACS,YAAY,EAA3C,qBAAqB;IACL,QAAQ,EAAxB,MAAM;IACU,WAAW,EAA3B,MAAM;IACU,KAAK,EAArB,MAAM;IACW,KAAK;IACQ,MAAM;IACnB,MAAM;IACN,aAAa;IACS,mBAAmB;CACnE,GAAS,MAAM,CA+BjB;AAED;;;;;;;;;;GAUG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,4KAdG;IAAqC,SAAS,EAArC,kBAAkB;IACD,IAAI;IACJ,WAAW;IACX,YAAY;IACZ,QAAQ;IACR,YAAY;IACZ,KAAK;IACL,YAAY;IACZ,UAAU;IACV,QAAQ;IACR,SAAS;IACD,cAAc;;;CAChD,GAAS,OAAO,CAAC,iBAAiB,CAAC,CAoErC;AAED;;;;;;;;;GASG;AAEH;;;;;;;;;;GAUG;AACH,4FAPG;IAAyB,QAAQ,EAAxB,MAAM;IACW,KAAK;IACL,QAAQ;IACR,SAAS;IACD,cAAc;;;CAChD,GAAS,OAAO,CAAC,sBAAsB,CAAC,CA+B1C;AAED;;;;;GAKG;AAEH;;;;;GAKG;AAEH;;;;;GAKG;AAEH;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,mHAVG;IAAwB,UAAU,EAA1B,MAAM;IACU,QAAQ,EAAxB,MAAM;IACW,QAAQ;IACR,eAAe;IACf,SAAS;IACD,cAAc;;;CAC/C,GAAU,OAAO,CAAC,oBAAoB,CAAC,CAuDzC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,8IArCG;IAAwB,UAAU,EAA1B,MAAM;IACU,QAAQ,EAAxB,MAAM;IACW,QAAQ;IACR,eAAe;IACf,SAAS;IACT,SAAS;IACD,cAAc;;;IACU,MAAM,aAA9C,oBAAoB,KAAK,IAAI;CAC9C,GAAU,OAAO,CAAC,iBAAiB,CAAC,CA0EtC;;;;oCA5ZY,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,eAAe,GAAG,gBAAgB,GAAG,qBAAqB;;;;kCAKzG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,gBAAgB;;;;2CAK/C,MAAM,GAAG,OAAO;;;;iCAKhB,oBAAoB,GAAG,eAAe,GAAG,oBAAoB,GAAG,OAAO,sBAAsB;;kBAsD5F,MAAM;gBACN,MAAM;gBACN,MAAM;oBACN,MAAM;YACN,MAAM;eACN,MAAM;iBACN,MAAM;;;;;;iBA4FN,MAAM;;;;eACN,MAAM;;;;sBACN,MAAM;;;;gCACN,MAAM;;;;gBACN,MAAM;;;;cACN,MAAM;;;;;;;;;YAgDN,SAAS;;;;cACT,MAAM;;;;;;;;;YAMN,WAAW;;;;cACX,MAAM;;;;;;;;;YAMN,SAAS;;;;YACT,iBAAiB;;;;;mCAKlB,qBAAqB,GAAG,sBAAsB,GAAG,qBAAqB;uCAxPK,gBAAgB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["auth.js"],"names":[],"mappings":"AAWA;;;GAGG;AAEH;;;GAGG;AAEH;;;GAGG;AAEH;;;GAGG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,4JAbG;IAA0B,QAAQ;IACR,KAAK;IACS,YAAY,EAA3C,qBAAqB;IACL,QAAQ,EAAxB,MAAM;IACU,WAAW,EAA3B,MAAM;IACU,KAAK,EAArB,MAAM;IACW,KAAK;IACQ,MAAM;IACnB,MAAM;IACN,aAAa;IACS,mBAAmB;CACnE,GAAS,MAAM,CA+BjB;AAED;;;;;;;;;;GAUG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,4KAdG;IAAqC,SAAS,EAArC,kBAAkB;IACD,IAAI;IACJ,WAAW;IACX,YAAY;IACZ,QAAQ;IACR,YAAY;IACZ,KAAK;IACL,YAAY;IACZ,UAAU;IACV,QAAQ;IACR,SAAS;IACD,cAAc;;;CAChD,GAAS,OAAO,CAAC,iBAAiB,CAAC,CA8ErC;AAED;;;;;;;;;GASG;AAEH;;;;;;;;;;GAUG;AACH,4FAPG;IAAyB,QAAQ,EAAxB,MAAM;IACW,KAAK;IACL,QAAQ;IACR,SAAS;IACD,cAAc;;;CAChD,GAAS,OAAO,CAAC,sBAAsB,CAAC,CA+B1C;AAED;;;;;GAKG;AAEH;;;;;GAKG;AAEH;;;;;GAKG;AAEH;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,mHAVG;IAAwB,UAAU,EAA1B,MAAM;IACU,QAAQ,EAAxB,MAAM;IACW,QAAQ;IACR,eAAe;IACf,SAAS;IACD,cAAc;;;CAC/C,GAAU,OAAO,CAAC,oBAAoB,CAAC,CAuDzC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,8IArCG;IAAwB,UAAU,EAA1B,MAAM;IACU,QAAQ,EAAxB,MAAM;IACW,QAAQ;IACR,eAAe;IACf,SAAS;IACT,SAAS;IACD,cAAc;;;IACU,MAAM,aAA9C,oBAAoB,KAAK,IAAI;CAC9C,GAAU,OAAO,CAAC,iBAAiB,CAAC,CA0EtC;;;;oCAtaY,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,eAAe,GAAG,gBAAgB,GAAG,qBAAqB;;;;kCAKzG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,gBAAgB;;;;2CAK/C,MAAM,GAAG,OAAO;;;;iCAKhB,oBAAoB,GAAG,eAAe,GAAG,oBAAoB,GAAG,OAAO,sBAAsB;;kBAsD5F,MAAM;gBACN,MAAM;gBACN,MAAM;oBACN,MAAM;YACN,MAAM;eACN,MAAM;iBACN,MAAM;;;;;;iBAsGN,MAAM;;;;eACN,MAAM;;;;sBACN,MAAM;;;;gCACN,MAAM;;;;gBACN,MAAM;;;;cACN,MAAM;;;;;;;;;YAgDN,SAAS;;;;cACT,MAAM;;;;;;;;;YAMN,WAAW;;;;cACX,MAAM;;;;;;;;;YAMN,SAAS;;;;YACT,iBAAiB;;;;;mCAKlB,qBAAqB,GAAG,sBAAsB,GAAG,qBAAqB;uCAlQK,gBAAgB"}
@@ -158,14 +158,24 @@ export async function exchangeToken ({
158
158
  // For device_code grant, always parse JSON first so error details are available
159
159
  // 403 errors with authorization_pending/slow_down are expected during polling
160
160
  if (grantType === DEVICE_CODE_GRANT_TYPE) {
161
- /** @type {any} */
162
- const rawResponse = await response.body.json()
161
+ const textBody = await response.body.text()
162
+ let jsonBody = null
163
+
164
+ try {
165
+ jsonBody = JSON.parse(textBody)
166
+ } catch {
167
+ jsonBody = null
168
+ }
163
169
 
164
170
  if (response.statusCode > 299) {
165
- throw new YotoAPIError(response, rawResponse, { grantType })
171
+ throw new YotoAPIError(response, textBody, jsonBody, { grantType })
172
+ }
173
+
174
+ if (!jsonBody || typeof jsonBody !== 'object') {
175
+ throw new Error('OAuth token response did not contain valid JSON')
166
176
  }
167
177
 
168
- const responseBody = /** @type {YotoTokenResponse} */ (rawResponse)
178
+ const responseBody = /** @type {YotoTokenResponse} */ (jsonBody)
169
179
  return responseBody
170
180
  }
171
181
 
@@ -303,7 +313,7 @@ export async function pollForDeviceToken ({
303
313
  }
304
314
  } catch (err) {
305
315
  const error = /** @type {any} */ (err)
306
- const errorCode = error.body?.error
316
+ const errorCode = error.jsonBody?.error
307
317
 
308
318
  // Handle recoverable polling states
309
319
  if (errorCode === 'authorization_pending') {
@@ -19,7 +19,7 @@ test('exchangeToken - refresh flow', async (t) => {
19
19
  (err) => {
20
20
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
21
21
  assert.ok(err.statusCode, 'Error should have statusCode')
22
- assert.ok(err.body, 'Error should have body')
22
+ assert.ok(err.jsonBody, 'Error should have jsonBody')
23
23
  return true
24
24
  }
25
25
  )
@@ -40,7 +40,7 @@ test('pollForDeviceToken', async (t) => {
40
40
  (err) => {
41
41
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
42
42
  // @ts-expect-error
43
- assert.strictEqual(err.body?.error, 'invalid_grant', 'Should be invalid_grant error')
43
+ assert.strictEqual(err.jsonBody?.error, 'invalid_grant', 'Should be invalid_grant error')
44
44
  assert.ok(err.statusCode, 'Error should have statusCode')
45
45
  return true
46
46
  }
@@ -62,7 +62,7 @@ test('pollForDeviceToken', async (t) => {
62
62
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
63
63
  // Will get invalid_grant for malformed codes
64
64
  // @ts-expect-error
65
- assert.ok(err.body?.error, 'Should have error code')
65
+ assert.ok(err.jsonBody?.error, 'Should have error code')
66
66
  return true
67
67
  }
68
68
  )
@@ -79,7 +79,7 @@ test('getUserMyoContent', async (t) => {
79
79
  (err) => {
80
80
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
81
81
  assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
82
- assert.ok(err.body, 'Error should have body')
82
+ assert.ok(err.jsonBody, 'Error should have jsonBody')
83
83
  return true
84
84
  }
85
85
  )
@@ -43,7 +43,7 @@ test('getDevices', async (t) => {
43
43
  (err) => {
44
44
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
45
45
  assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
46
- assert.ok(err.body, 'Error should have body')
46
+ assert.ok(err.jsonBody, 'Error should have jsonBody')
47
47
  return true
48
48
  }
49
49
  )
@@ -185,7 +185,7 @@ test('getDeviceStatus', async (t) => {
185
185
  (err) => {
186
186
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
187
187
  assert.equal(err.statusCode, 404, 'Should return 404 for invalid device ID')
188
- assert.ok(err.body, 'Error should have body')
188
+ assert.ok(err.jsonBody, 'Error should have jsonBody')
189
189
  return true
190
190
  }
191
191
  )
@@ -205,7 +205,7 @@ test('getDeviceStatus', async (t) => {
205
205
  (err) => {
206
206
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
207
207
  assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
208
- assert.ok(err.body, 'Error should have body')
208
+ assert.ok(err.jsonBody, 'Error should have jsonBody')
209
209
  return true
210
210
  }
211
211
  )
@@ -455,7 +455,7 @@ test('getDeviceConfig', async (t) => {
455
455
  (err) => {
456
456
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
457
457
  assert.ok(err.statusCode >= 400, 'Should return error status code for invalid device ID')
458
- assert.ok(err.body, 'Error should have body')
458
+ assert.ok(err.jsonBody, 'Error should have jsonBody')
459
459
  return true
460
460
  }
461
461
  )
@@ -475,7 +475,7 @@ test('getDeviceConfig', async (t) => {
475
475
  (err) => {
476
476
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
477
477
  assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
478
- assert.ok(err.body, 'Error should have body')
478
+ assert.ok(err.jsonBody, 'Error should have jsonBody')
479
479
  return true
480
480
  }
481
481
  )
@@ -53,7 +53,7 @@ test('getGroups', async (t) => {
53
53
  (err) => {
54
54
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
55
55
  assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
56
- assert.ok(err.body, 'Error should have body')
56
+ assert.ok(err.jsonBody, 'Error should have jsonBody')
57
57
  return true
58
58
  }
59
59
  )
@@ -37,7 +37,7 @@ test('getFamilyImages', async (t) => {
37
37
  (err) => {
38
38
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
39
39
  assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
40
- assert.ok(err.body, 'Error should have body')
40
+ assert.ok(err.jsonBody, 'Error should have jsonBody')
41
41
  return true
42
42
  }
43
43
  )
@@ -164,7 +164,7 @@ test('getAFamilyImage', async (t) => {
164
164
  (err) => {
165
165
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
166
166
  assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
167
- assert.ok(err.body, 'Error should have body')
167
+ assert.ok(err.jsonBody, 'Error should have jsonBody')
168
168
  return true
169
169
  }
170
170
  )
@@ -46,12 +46,14 @@ export function handleBadResponse(response: Dispatcher.ResponseData, extra?: any
46
46
  export class YotoAPIError extends Error {
47
47
  /**
48
48
  * @param {Dispatcher.ResponseData} response A undici Response
49
- * @param {string | object} body response body
49
+ * @param {string} textBody response body as text
50
+ * @param {unknown | null} jsonBody parsed response body (or null if invalid JSON)
50
51
  * @param {any} [extra] any extra info to attach to the error
51
52
  */
52
- constructor(response: Dispatcher.ResponseData, body: string | object, extra?: any);
53
+ constructor(response: Dispatcher.ResponseData, textBody: string, jsonBody: unknown | null, extra?: any);
53
54
  /** @type { number } */ statusCode: number;
54
- /** @type {string | object } */ body: string | object;
55
+ /** @type {string} */ textBody: string;
56
+ /** @type {unknown | null} */ jsonBody: unknown | null;
55
57
  /** @type {any} */ extra: any;
56
58
  }
57
59
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["helpers.js"],"names":[],"mappings":"AAOA;;;GAGG;AAEH;;;;GAIG;AACH,yCAHG;IAAyB,SAAS;IACD,cAAc;;;CACjD;;;EAWA;AAED;;;;GAIG;AACH,sEAHG;IAAwB,WAAW,EAA1B,MAAM;IACS,SAAS;CACnC;;;;EAMA;AAED;;;;;;;GAOG;AACH,iDAJW,cAAc,mBACd,cAAc,GACZ,MAAM,CAoBlB;AAED;;;GAGG;AACH,4CAHY,uBAAuB,UACvB,GAAG,iBAWd;AAED;IAKE;;;;OAIG;IACH,sBAJY,uBAAuB,QACvB,MAAM,GAAG,MAAM,UACf,GAAG,EAUd;IAjBD,uBAAuB,CAAC,YAAZ,MAAM,CAAgB;IAClC,+BAA+B,CAAC,MAArB,MAAM,GAAG,MAAM,CAAU;IACpC,kBAAkB,CAAC,OAAR,GAAG,CAAU;CAgBzB;;;;6BA9FY,WAAW,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;gCARxB,QAAQ;6BACX,QAAQ"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["helpers.js"],"names":[],"mappings":"AAOA;;;GAGG;AAEH;;;;GAIG;AACH,yCAHG;IAAyB,SAAS;IACD,cAAc;;;CACjD;;;EAWA;AAED;;;;GAIG;AACH,sEAHG;IAAwB,WAAW,EAA1B,MAAM;IACS,SAAS;CACnC;;;;EAMA;AAED;;;;;;;GAOG;AACH,iDAJW,cAAc,mBACd,cAAc,GACZ,MAAM,CAoBlB;AAED;;;GAGG;AACH,4CAHY,uBAAuB,UACvB,GAAG,iBAed;AAED;IAME;;;;;OAKG;IACH,sBALY,uBAAuB,YACvB,MAAM,YACN,OAAO,GAAG,IAAI,UACd,GAAG,EAWd;IApBD,uBAAuB,CAAC,YAAZ,MAAM,CAAgB;IAClC,qBAAqB,CAAC,UAAX,MAAM,CAAa;IAC9B,6BAA6B,CAAC,UAAnB,OAAO,GAAG,IAAI,CAAa;IACtC,kBAAkB,CAAC,OAAR,GAAG,CAAU;CAkBzB;;;;6BArGY,WAAW,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;gCARxB,QAAQ;6BACX,QAAQ"}
@@ -73,32 +73,39 @@ export function mergeRequestOptions (baseOptions, requestOptions) {
73
73
  */
74
74
  export async function handleBadResponse (response, extra) {
75
75
  if (response.statusCode > 299) {
76
- const contentTypeHeaders = response.headers['Content-Type']
77
- const contentType = Array.isArray(contentTypeHeaders) ? contentTypeHeaders[0] : contentTypeHeaders
78
- const isJSON = contentType && contentType.match(/json/)
79
- /** @type { any } */
80
- const body = isJSON ? await response.body.json() : await response.body.text()
81
- throw new YotoAPIError(response, body, extra)
76
+ const textBody = await response.body.text()
77
+ let jsonBody = null
78
+
79
+ try {
80
+ jsonBody = JSON.parse(textBody)
81
+ } catch {
82
+ jsonBody = null
83
+ }
84
+
85
+ throw new YotoAPIError(response, textBody, jsonBody, extra)
82
86
  }
83
87
  }
84
88
 
85
89
  export class YotoAPIError extends Error {
86
90
  /** @type { number } */ statusCode
87
- /** @type {string | object } */ body
91
+ /** @type {string} */ textBody
92
+ /** @type {unknown | null} */ jsonBody
88
93
  /** @type {any} */ extra
89
94
 
90
95
  /**
91
96
  * @param {Dispatcher.ResponseData} response A undici Response
92
- * @param {string | object} body response body
97
+ * @param {string} textBody response body as text
98
+ * @param {unknown | null} jsonBody parsed response body (or null if invalid JSON)
93
99
  * @param {any} [extra] any extra info to attach to the error
94
100
  */
95
- constructor (response, body, extra) {
101
+ constructor (response, textBody, jsonBody, extra) {
96
102
  super('Unexpected response status code')
97
103
  this.name = this.constructor.name
98
104
  Error.captureStackTrace(this, this.constructor)
99
105
 
100
106
  this.statusCode = response.statusCode
101
- this.body = body
107
+ this.textBody = textBody
108
+ this.jsonBody = jsonBody
102
109
  this.extra = extra
103
110
  }
104
111
  }
@@ -50,7 +50,7 @@ test('getPublicIcons', async (t) => {
50
50
  (err) => {
51
51
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
52
52
  assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
53
- assert.ok(err.body, 'Error should have body')
53
+ assert.ok(err.jsonBody, 'Error should have jsonBody')
54
54
  return true
55
55
  }
56
56
  )
@@ -96,7 +96,7 @@ test('getUserIcons', async (t) => {
96
96
  (err) => {
97
97
  assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
98
98
  assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
99
- assert.ok(err.body, 'Error should have body')
99
+ assert.ok(err.jsonBody, 'Error should have jsonBody')
100
100
  return true
101
101
  }
102
102
  )
@@ -1 +1 @@
1
- {"version":3,"file":"device-model-test-helpers.d.ts","sourceRoot":"","sources":["device-model-test-helpers.js"],"names":[],"mappings":"AAMA;;;;;GAKG;AACH,qCALW,OAAO,CAAC,OAAO,CAAC,aAChB,MAAM,SACN,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAS5B;AAED;;;GAGG;AACH,yCAHW,eAAe,GACb,OAAO,CAAC,IAAI,CAAC,CAUzB;AAED;;GAEG;AACH,0CAFW,qBAAqB,QAkD/B;AAED;;GAEG;AACH,8CAFW,iBAAiB,QAkB3B;AAED;;;GAGG;AACH,+BAHW,MAAM,GAAG,SAAS,GAChB,MAAM,CAIlB;qCAnH8E,mBAAmB;2CAAnB,mBAAmB;uCAAnB,mBAAmB"}
1
+ {"version":3,"file":"device-model-test-helpers.d.ts","sourceRoot":"","sources":["device-model-test-helpers.js"],"names":[],"mappings":"AAMA;;;;;GAKG;AACH,qCALW,OAAO,CAAC,OAAO,CAAC,aAChB,MAAM,SACN,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAS5B;AAED;;;GAGG;AACH,yCAHW,eAAe,GACb,OAAO,CAAC,IAAI,CAAC,CAUzB;AAED;;GAEG;AACH,0CAFW,qBAAqB,QAkD/B;AAED;;GAEG;AACH,8CAFW,iBAAiB,QAwB3B;AAED;;;GAGG;AACH,+BAHW,MAAM,GAAG,SAAS,GAChB,MAAM,CAIlB;qCAzH8E,mBAAmB;2CAAnB,mBAAmB;uCAAnB,mBAAmB"}
@@ -94,6 +94,12 @@ export function assertPlaybackShape (playback) {
94
94
  assert.equal(typeof playback.updatedAt, 'string', 'playback.updatedAt should be string')
95
95
 
96
96
  assert.ok(playback.cardId === null || typeof playback.cardId === 'string', 'playback.cardId should be string or null')
97
+ assert.ok(playback.cardTitle === null || typeof playback.cardTitle === 'string', 'playback.cardTitle should be string or null')
98
+ assert.ok(playback.cardSlug === null || typeof playback.cardSlug === 'string', 'playback.cardSlug should be string or null')
99
+ assert.ok(playback.cardCoverImageUrl === null || typeof playback.cardCoverImageUrl === 'string', 'playback.cardCoverImageUrl should be string or null')
100
+ assert.ok(playback.cardAuthor === null || typeof playback.cardAuthor === 'string', 'playback.cardAuthor should be string or null')
101
+ assert.ok(playback.cardReadBy === null || typeof playback.cardReadBy === 'string', 'playback.cardReadBy should be string or null')
102
+ assert.ok(playback.cardDurationSeconds === null || typeof playback.cardDurationSeconds === 'number', 'playback.cardDurationSeconds should be number or null')
97
103
  assert.ok(playback.source === null || typeof playback.source === 'string', 'playback.source should be string or null')
98
104
  assert.ok(playback.playbackStatus === null || typeof playback.playbackStatus === 'string', 'playback.playbackStatus should be string or null')
99
105
  assert.ok(playback.trackTitle === null || typeof playback.trackTitle === 'string', 'playback.trackTitle should be string or null')
@@ -1 +1 @@
1
- {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["token.js"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AAEH;;;;;;GAMG;AAEH;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AAEH;;;;;;;;;;;GAWG;AAEH;IAwBE;;OAEG;IACH,oFAFW,oBAAoB,EAuB9B;IAED;;;;OAIG;IACH,kBAHa,OAAO,CAAC,MAAM,CAAC,CAkB3B;IAyGD;;OAEG;IACH,wBAGC;IAED;;OAEG;IACH,yBAQC;IAED;;;OAGG;IACH,wBAFa,OAAO,CAInB;IAED;;;OAGG;IACH,gBAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,mBAFa,MAAM,CAIlB;IAiED;;;OAGG;IACH,WAFa,OAAO,CASnB;IAED;;;OAGG;IACH,gBAFa,MAAM,CAIlB;IAED;;;OAGG;IACH,oBAFa,MAAM,CAKlB;IAED;;;;;OAKG;IACH,WAHa,OAAO,CAAC,mBAAmB,CAAC,CAUxC;;CACF;;;;;cAzXa,MAAM;;;;kBACN,MAAM;;;;iBACN,MAAM;;;;oBACN,qBAAqB;;;;oBACrB,MAAM;;;;;;;oCAQP,CAAC,mBAAmB,EAAE,mBAAmB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;;;;cAKjE,MAAM;;;;wBACN,MAAM;;;;yBACN,MAAM;;;;sBACN,MAAM;;;;qBACN,MAAM;;;;sBACN,MAAM;;;;mBACN,MAAM;;;;;uCAKP;IACZ,eAAmB,EAAE,EAAE,CAAC;IACxB,iBAAqB,EAAE,CAAC,mBAAmB,CAAC,CAAC;IAC7C,eAAmB,EAAE,CAAC,KAAK,CAAC,CAAC;IAC7B,SAAa,EAAE,CAAC,KAAK,CAAC,CAAA;CACnB;6BA1CyB,aAAa"}
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["token.js"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AAEH;;;;;;GAMG;AAEH;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AAEH;;;;;;;;;;;GAWG;AAEH;IAwBE;;OAEG;IACH,oFAFW,oBAAoB,EAuB9B;IAED;;;;OAIG;IACH,kBAHa,OAAO,CAAC,MAAM,CAAC,CAkB3B;IA2GD;;OAEG;IACH,wBAGC;IAED;;OAEG;IACH,yBAQC;IAED;;;OAGG;IACH,wBAFa,OAAO,CAInB;IAED;;;OAGG;IACH,gBAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,mBAFa,MAAM,CAIlB;IAiED;;;OAGG;IACH,WAFa,OAAO,CASnB;IAED;;;OAGG;IACH,gBAFa,MAAM,CAIlB;IAED;;;OAGG;IACH,oBAFa,MAAM,CAKlB;IAED;;;;;OAKG;IACH,WAHa,OAAO,CAAC,mBAAmB,CAAC,CAUxC;;CACF;;;;;cA3Xa,MAAM;;;;kBACN,MAAM;;;;iBACN,MAAM;;;;oBACN,qBAAqB;;;;oBACrB,MAAM;;;;;;;oCAQP,CAAC,mBAAmB,EAAE,mBAAmB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;;;;;cAKjE,MAAM;;;;wBACN,MAAM;;;;yBACN,MAAM;;;;sBACN,MAAM;;;;qBACN,MAAM;;;;sBACN,MAAM;;;;mBACN,MAAM;;;;;uCAKP;IACZ,eAAmB,EAAE,EAAE,CAAC;IACxB,iBAAqB,EAAE,CAAC,mBAAmB,CAAC,CAAC;IAC7C,eAAmB,EAAE,CAAC,KAAK,CAAC,CAAC;IAC7B,SAAa,EAAE,CAAC,KAAK,CAAC,CAAA;CACnB;6BA1CyB,aAAa"}
package/lib/token.js CHANGED
@@ -214,12 +214,14 @@ export class RefreshableToken extends EventEmitter {
214
214
  'refresh_token_expired'
215
215
  ]
216
216
 
217
- if (error.body?.error && invalidRefreshErrors.includes(error.body.error)) {
217
+ const errorCode = error.jsonBody?.error
218
+ if (errorCode && invalidRefreshErrors.includes(errorCode)) {
218
219
  // Mark this token as permanently invalid
219
220
  this.#invalid = true
220
221
  this.#clearAutoRefreshTimeout()
221
222
  const statusCode = error.statusCode ? ` (${error.statusCode})` : ''
222
- const invalidError = new Error(`Refresh token is invalid or expired${statusCode}: ${error.body.error}${error.body.error_description ? ` - ${error.body.error_description}` : ''}`)
223
+ const errorDescription = error.jsonBody?.error_description
224
+ const invalidError = new Error(`Refresh token is invalid or expired${statusCode}: ${errorCode}${errorDescription ? ` - ${errorDescription}` : ''}`)
223
225
  this.emit('invalid', invalidError)
224
226
  throw invalidError
225
227
  }
@@ -102,6 +102,12 @@ export const YotoDeviceModelConfigType: {};
102
102
  * Playback state from MQTT events
103
103
  * @typedef {Object} YotoPlaybackState
104
104
  * @property {string | null} cardId - Currently playing card ID TODO: Figure out name of card
105
+ * @property {string | null} cardTitle - Card title (from cached content)
106
+ * @property {string | null} cardSlug - Card slug (from cached content)
107
+ * @property {string | null} cardCoverImageUrl - Card cover image URL (from cached content)
108
+ * @property {string | null} cardAuthor - Card author (from cached content)
109
+ * @property {string | null} cardReadBy - Card narrator/reader (from cached content)
110
+ * @property {number | null} cardDurationSeconds - Total card duration in seconds (from cached content)
105
111
  * @property {string | null} source - Playback source (e.g., 'card', 'remote', 'MQTT') TODO: Figure out what 'mqtt' source means. Card means card, remote means remotly played card.
106
112
  * @property {PlaybackStatus | null} playbackStatus - Playback status
107
113
  * @property {string | null} trackTitle - Current track title
@@ -135,7 +141,12 @@ export const YotoPlaybackStateType: {};
135
141
  * Cached card metadata for playback enrichment.
136
142
  * @typedef {Object} YotoCardCacheEntry
137
143
  * @property {string} cardId
138
- * @property {string} title
144
+ * @property {string} cardTitle
145
+ * @property {string | null} cardSlug
146
+ * @property {string | null} cardCoverImageUrl
147
+ * @property {string | null} cardAuthor
148
+ * @property {string | null} cardReadBy
149
+ * @property {number | null} cardDurationSeconds
139
150
  * @property {Map<string, YotoCardCacheChapterInfo>} chaptersByKey
140
151
  * @property {Map<string, YotoCardCacheTrackInfo>} tracksByKey
141
152
  */
@@ -824,6 +835,30 @@ export type YotoPlaybackState = {
824
835
  * - Currently playing card ID TODO: Figure out name of card
825
836
  */
826
837
  cardId: string | null;
838
+ /**
839
+ * - Card title (from cached content)
840
+ */
841
+ cardTitle: string | null;
842
+ /**
843
+ * - Card slug (from cached content)
844
+ */
845
+ cardSlug: string | null;
846
+ /**
847
+ * - Card cover image URL (from cached content)
848
+ */
849
+ cardCoverImageUrl: string | null;
850
+ /**
851
+ * - Card author (from cached content)
852
+ */
853
+ cardAuthor: string | null;
854
+ /**
855
+ * - Card narrator/reader (from cached content)
856
+ */
857
+ cardReadBy: string | null;
858
+ /**
859
+ * - Total card duration in seconds (from cached content)
860
+ */
861
+ cardDurationSeconds: number | null;
827
862
  /**
828
863
  * - Playback source (e.g., 'card', 'remote', 'MQTT') TODO: Figure out what 'mqtt' source means. Card means card, remote means remotly played card.
829
864
  */
@@ -895,7 +930,12 @@ export type YotoCardCacheTrackInfo = {
895
930
  */
896
931
  export type YotoCardCacheEntry = {
897
932
  cardId: string;
898
- title: string;
933
+ cardTitle: string;
934
+ cardSlug: string | null;
935
+ cardCoverImageUrl: string | null;
936
+ cardAuthor: string | null;
937
+ cardReadBy: string | null;
938
+ cardDurationSeconds: number | null;
899
939
  chaptersByKey: Map<string, YotoCardCacheChapterInfo>;
900
940
  tracksByKey: Map<string, YotoCardCacheTrackInfo>;
901
941
  };
@@ -1 +1 @@
1
- {"version":3,"file":"yoto-device.d.ts","sourceRoot":"","sources":["yoto-device.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"yoto-device.d.ts","sourceRoot":"","sources":["yoto-device.js"],"names":[],"mappings":"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"}
@@ -20,6 +20,7 @@
20
20
  import { EventEmitter } from 'events'
21
21
  import fastq from 'fastq'
22
22
  import QuickLRU from 'quick-lru'
23
+ import { YotoAPIError } from './api-endpoints/helpers.js'
23
24
  import { parseTemperature } from './helpers/temperature.js'
24
25
  import { detectPowerState } from './helpers/power-state.js'
25
26
  import { typedKeys } from './helpers/typed-keys.js'
@@ -399,6 +400,12 @@ export const YotoDeviceModelConfigType = {}
399
400
  * Playback state from MQTT events
400
401
  * @typedef {Object} YotoPlaybackState
401
402
  * @property {string | null} cardId - Currently playing card ID TODO: Figure out name of card
403
+ * @property {string | null} cardTitle - Card title (from cached content)
404
+ * @property {string | null} cardSlug - Card slug (from cached content)
405
+ * @property {string | null} cardCoverImageUrl - Card cover image URL (from cached content)
406
+ * @property {string | null} cardAuthor - Card author (from cached content)
407
+ * @property {string | null} cardReadBy - Card narrator/reader (from cached content)
408
+ * @property {number | null} cardDurationSeconds - Total card duration in seconds (from cached content)
402
409
  * @property {string | null} source - Playback source (e.g., 'card', 'remote', 'MQTT') TODO: Figure out what 'mqtt' source means. Card means card, remote means remotly played card.
403
410
  * @property {PlaybackStatus | null} playbackStatus - Playback status
404
411
  * @property {string | null} trackTitle - Current track title
@@ -439,7 +446,12 @@ export const YotoPlaybackStateType = {}
439
446
  * Cached card metadata for playback enrichment.
440
447
  * @typedef {Object} YotoCardCacheEntry
441
448
  * @property {string} cardId
442
- * @property {string} title
449
+ * @property {string} cardTitle
450
+ * @property {string | null} cardSlug
451
+ * @property {string | null} cardCoverImageUrl
452
+ * @property {string | null} cardAuthor
453
+ * @property {string | null} cardReadBy
454
+ * @property {number | null} cardDurationSeconds
443
455
  * @property {Map<string, YotoCardCacheChapterInfo>} chaptersByKey
444
456
  * @property {Map<string, YotoCardCacheTrackInfo>} tracksByKey
445
457
  */
@@ -1456,12 +1468,16 @@ export class YotoDeviceModel extends EventEmitter {
1456
1468
  #updateStatusFromFullStatus (fullStatus) {
1457
1469
  let statusChanged = false
1458
1470
  let configChanged = false
1471
+ let playbackChanged = false
1459
1472
  const wasOnline = this.#deviceOnline
1460
- const { status, config } = this.#state
1473
+ const { status, config, playback } = this.#state
1474
+ const previousPlaybackCardId = playback.cardId
1461
1475
  /** @type {Set<keyof YotoDeviceStatus>} */
1462
1476
  const changedFields = new Set()
1463
1477
  /** @type {Set<keyof YotoDeviceModelConfig>} */
1464
1478
  const configChangedFields = new Set()
1479
+ /** @type {Set<keyof YotoPlaybackState>} */
1480
+ const playbackChangedFields = new Set()
1465
1481
 
1466
1482
  /**
1467
1483
  * Handler function for each status field
@@ -1826,6 +1842,20 @@ export class YotoDeviceModel extends EventEmitter {
1826
1842
  handleField(key, fullStatus)
1827
1843
  }
1828
1844
 
1845
+ if (changedFields.has('activeCardId') && previousPlaybackCardId !== status.activeCardId) {
1846
+ playback.cardId = status.activeCardId
1847
+ playbackChangedFields.add('cardId')
1848
+ playbackChanged = true
1849
+
1850
+ if (clearCardCachePlaybackFields(playback, playbackChangedFields)) {
1851
+ playbackChanged = true
1852
+ }
1853
+
1854
+ if (this.#applyCardCacheToPlayback(playback.cardId, playback, playbackChangedFields)) {
1855
+ playbackChanged = true
1856
+ }
1857
+ }
1858
+
1829
1859
  // Only emit if something actually changed
1830
1860
  if (statusChanged) {
1831
1861
  // Update metadata
@@ -1845,6 +1875,12 @@ export class YotoDeviceModel extends EventEmitter {
1845
1875
  this.emit('configUpdate', this.config, configChangedFields)
1846
1876
  }
1847
1877
 
1878
+ if (playbackChanged) {
1879
+ playback.updatedAt = new Date().toISOString()
1880
+ this.#state.lastUpdate.playback = Date.now()
1881
+ this.emit('playbackUpdate', this.playback, playbackChangedFields)
1882
+ }
1883
+
1848
1884
  // Check if device went offline (transition from online to offline)
1849
1885
  if (wasOnline && !this.#deviceOnline) {
1850
1886
  this.emit('offline', { reason: 'http-status', source: 'http' })
@@ -1859,10 +1895,14 @@ export class YotoDeviceModel extends EventEmitter {
1859
1895
  */
1860
1896
  #updateStatusFromStatusResponse (statusResponse) {
1861
1897
  let statusChanged = false
1898
+ let playbackChanged = false
1862
1899
  const wasOnline = this.#deviceOnline
1863
- const { status } = this.#state
1900
+ const { status, playback } = this.#state
1901
+ const previousPlaybackCardId = playback.cardId
1864
1902
  /** @type {Set<keyof YotoDeviceStatus>} */
1865
1903
  const changedFields = new Set()
1904
+ /** @type {Set<keyof YotoPlaybackState>} */
1905
+ const playbackChangedFields = new Set()
1866
1906
 
1867
1907
  /**
1868
1908
  * Handler function for each status field
@@ -2103,6 +2143,20 @@ export class YotoDeviceModel extends EventEmitter {
2103
2143
  handleField(key, statusResponse)
2104
2144
  }
2105
2145
 
2146
+ if (changedFields.has('activeCardId') && previousPlaybackCardId !== status.activeCardId) {
2147
+ playback.cardId = status.activeCardId
2148
+ playbackChangedFields.add('cardId')
2149
+ playbackChanged = true
2150
+
2151
+ if (clearCardCachePlaybackFields(playback, playbackChangedFields)) {
2152
+ playbackChanged = true
2153
+ }
2154
+
2155
+ if (this.#applyCardCacheToPlayback(playback.cardId, playback, playbackChangedFields)) {
2156
+ playbackChanged = true
2157
+ }
2158
+ }
2159
+
2106
2160
  // Only emit if something actually changed
2107
2161
  if (statusChanged) {
2108
2162
  // Update metadata
@@ -2115,6 +2169,12 @@ export class YotoDeviceModel extends EventEmitter {
2115
2169
  this.emit('statusUpdate', this.status, 'http', changedFields)
2116
2170
  }
2117
2171
 
2172
+ if (playbackChanged) {
2173
+ playback.updatedAt = new Date().toISOString()
2174
+ this.#state.lastUpdate.playback = Date.now()
2175
+ this.emit('playbackUpdate', this.playback, playbackChangedFields)
2176
+ }
2177
+
2118
2178
  // Check if device went offline (transition from online to offline)
2119
2179
  if (wasOnline && !this.#deviceOnline) {
2120
2180
  this.emit('offline', { reason: 'http-status', source: 'http' })
@@ -3187,7 +3247,25 @@ export class YotoDeviceModel extends EventEmitter {
3187
3247
  }
3188
3248
 
3189
3249
  return entry
3190
- } catch {
3250
+ } catch (err) {
3251
+ if (err instanceof YotoAPIError) {
3252
+ const errorBody = /** @type {{ error?: { code?: string, message?: string } } | null} */ (err.jsonBody)
3253
+ const statusCode = err.statusCode
3254
+ const extraCardId = err.extra?.cardId
3255
+ const bodyCode = errorBody?.error?.code
3256
+ const bodyMessage = errorBody?.error?.message
3257
+
3258
+ const ignoreForbiddenCard = statusCode === 403 &&
3259
+ (!extraCardId || extraCardId === task.cardId) &&
3260
+ bodyCode === 'forbidden' &&
3261
+ bodyMessage?.includes('Access to card') &&
3262
+ bodyMessage?.includes('forbidden')
3263
+
3264
+ if (ignoreForbiddenCard) return null
3265
+ }
3266
+
3267
+ const error = err instanceof Error ? err : new Error(String(err))
3268
+ this.emit('error', error)
3191
3269
  return null
3192
3270
  } finally {
3193
3271
  this.#cardCacheQueueTasks.delete(task.cardId)
@@ -3397,6 +3475,9 @@ export class YotoDeviceModel extends EventEmitter {
3397
3475
  }
3398
3476
 
3399
3477
  if (previousCardId !== playback.cardId) {
3478
+ if (clearCardCachePlaybackFields(playback, playbackChangedFields)) {
3479
+ playbackChanged = true
3480
+ }
3400
3481
  if (this.#applyCardCacheToPlayback(playback.cardId, playback, playbackChangedFields)) {
3401
3482
  playbackChanged = true
3402
3483
  }
@@ -3433,6 +3514,12 @@ export class YotoDeviceModel extends EventEmitter {
3433
3514
  function createEmptyPlaybackState () {
3434
3515
  return {
3435
3516
  cardId: null,
3517
+ cardTitle: null,
3518
+ cardSlug: null,
3519
+ cardCoverImageUrl: null,
3520
+ cardAuthor: null,
3521
+ cardReadBy: null,
3522
+ cardDurationSeconds: null,
3436
3523
  source: null,
3437
3524
  playbackStatus: null,
3438
3525
  trackTitle: null,
@@ -3481,14 +3568,96 @@ function buildCardCacheEntry (card) {
3481
3568
  }
3482
3569
  }
3483
3570
 
3571
+ const cardCoverImageUrl = card.metadata?.cover?.imageL ?? null
3572
+ const cardAuthor = card.metadata?.author ?? null
3573
+ const cardReadBy = card.metadata?.readBy ?? null
3574
+ const cardDurationSeconds = Number.isFinite(card.metadata?.media?.duration) ? card.metadata.media.duration : null
3575
+ const cardSlug = card.slug || null
3576
+
3484
3577
  return {
3485
3578
  cardId: card.cardId,
3486
- title: card.title,
3579
+ cardTitle: card.title,
3580
+ cardSlug,
3581
+ cardCoverImageUrl,
3582
+ cardAuthor,
3583
+ cardReadBy,
3584
+ cardDurationSeconds,
3487
3585
  chaptersByKey,
3488
3586
  tracksByKey
3489
3587
  }
3490
3588
  }
3491
3589
 
3590
+ /**
3591
+ * @param {YotoPlaybackState} playback
3592
+ * @param {Set<keyof YotoPlaybackState>} changedFields
3593
+ * @returns {boolean}
3594
+ */
3595
+ function clearCardCachePlaybackFields (playback, changedFields) {
3596
+ let changed = false
3597
+
3598
+ if (!changedFields.has('cardTitle') && playback.cardTitle !== null) {
3599
+ playback.cardTitle = null
3600
+ changedFields.add('cardTitle')
3601
+ changed = true
3602
+ }
3603
+
3604
+ if (!changedFields.has('cardSlug') && playback.cardSlug !== null) {
3605
+ playback.cardSlug = null
3606
+ changedFields.add('cardSlug')
3607
+ changed = true
3608
+ }
3609
+
3610
+ if (!changedFields.has('cardCoverImageUrl') && playback.cardCoverImageUrl !== null) {
3611
+ playback.cardCoverImageUrl = null
3612
+ changedFields.add('cardCoverImageUrl')
3613
+ changed = true
3614
+ }
3615
+
3616
+ if (!changedFields.has('cardAuthor') && playback.cardAuthor !== null) {
3617
+ playback.cardAuthor = null
3618
+ changedFields.add('cardAuthor')
3619
+ changed = true
3620
+ }
3621
+
3622
+ if (!changedFields.has('cardReadBy') && playback.cardReadBy !== null) {
3623
+ playback.cardReadBy = null
3624
+ changedFields.add('cardReadBy')
3625
+ changed = true
3626
+ }
3627
+
3628
+ if (!changedFields.has('cardDurationSeconds') && playback.cardDurationSeconds !== null) {
3629
+ playback.cardDurationSeconds = null
3630
+ changedFields.add('cardDurationSeconds')
3631
+ changed = true
3632
+ }
3633
+
3634
+ if (!changedFields.has('trackTitle') && playback.trackTitle !== null) {
3635
+ playback.trackTitle = null
3636
+ changedFields.add('trackTitle')
3637
+ changed = true
3638
+ }
3639
+
3640
+ if (!changedFields.has('trackLength') && playback.trackLength !== null) {
3641
+ playback.trackLength = null
3642
+ changedFields.add('trackLength')
3643
+ changed = true
3644
+ }
3645
+
3646
+ if (!changedFields.has('chapterKey') && playback.chapterKey !== null) {
3647
+ playback.chapterKey = null
3648
+ changedFields.add('chapterKey')
3649
+ changed = true
3650
+ }
3651
+
3652
+ if (!changedFields.has('chapterTitle') && playback.chapterTitle !== null) {
3653
+ playback.chapterTitle = null
3654
+ changedFields.add('chapterTitle')
3655
+ changed = true
3656
+ }
3657
+
3658
+ return changed
3659
+ }
3660
+
3492
3661
  /**
3493
3662
  * @param {YotoCardCacheEntry} entry
3494
3663
  * @param {YotoPlaybackState} playback
@@ -3497,6 +3666,43 @@ function buildCardCacheEntry (card) {
3497
3666
  */
3498
3667
  function applyCardCacheToPlayback (entry, playback, changedFields) {
3499
3668
  let changed = false
3669
+
3670
+ if (playback.cardTitle !== entry.cardTitle) {
3671
+ playback.cardTitle = entry.cardTitle
3672
+ changedFields.add('cardTitle')
3673
+ changed = true
3674
+ }
3675
+
3676
+ if (playback.cardSlug !== entry.cardSlug) {
3677
+ playback.cardSlug = entry.cardSlug
3678
+ changedFields.add('cardSlug')
3679
+ changed = true
3680
+ }
3681
+
3682
+ if (playback.cardCoverImageUrl !== entry.cardCoverImageUrl) {
3683
+ playback.cardCoverImageUrl = entry.cardCoverImageUrl
3684
+ changedFields.add('cardCoverImageUrl')
3685
+ changed = true
3686
+ }
3687
+
3688
+ if (playback.cardAuthor !== entry.cardAuthor) {
3689
+ playback.cardAuthor = entry.cardAuthor
3690
+ changedFields.add('cardAuthor')
3691
+ changed = true
3692
+ }
3693
+
3694
+ if (playback.cardReadBy !== entry.cardReadBy) {
3695
+ playback.cardReadBy = entry.cardReadBy
3696
+ changedFields.add('cardReadBy')
3697
+ changed = true
3698
+ }
3699
+
3700
+ if (playback.cardDurationSeconds !== entry.cardDurationSeconds) {
3701
+ playback.cardDurationSeconds = entry.cardDurationSeconds
3702
+ changedFields.add('cardDurationSeconds')
3703
+ changed = true
3704
+ }
3705
+
3500
3706
  const { trackKey } = playback
3501
3707
 
3502
3708
  if (trackKey) {
@@ -21,6 +21,12 @@ const PLAYBACK_SAMPLE_SCAN_LIMIT = 5
21
21
  /**
22
22
  * @typedef {Object} PlaybackSample
23
23
  * @property {string} cardId
24
+ * @property {string} cardTitle
25
+ * @property {string | null} cardSlug
26
+ * @property {string | null} cardCoverImageUrl
27
+ * @property {string | null} cardAuthor
28
+ * @property {string | null} cardReadBy
29
+ * @property {number | null} cardDurationSeconds
24
30
  * @property {Map<string, { title: string, duration: number, chapterKey: string, chapterTitle: string }>} tracksByKey
25
31
  * @property {string} chapterKey
26
32
  * @property {string} chapterTitle
@@ -86,7 +92,14 @@ async function findPlaybackSample (client) {
86
92
 
87
93
  try {
88
94
  const contentResponse = await client.getContent({ cardId: card.cardId })
89
- const chapters = contentResponse.card.content.chapters
95
+ const contentCard = contentResponse.card
96
+ const metadata = contentCard.metadata
97
+ const cardCoverImageUrl = metadata?.cover?.imageL ?? null
98
+ const cardAuthor = metadata?.author ?? null
99
+ const cardReadBy = metadata?.readBy ?? null
100
+ const cardDurationSeconds = Number.isFinite(metadata?.media?.duration) ? metadata.media.duration : null
101
+ const cardSlug = contentCard.slug || null
102
+ const chapters = contentCard.content.chapters
90
103
  const firstChapter = chapters.find(candidate => candidate.tracks.length > 0)
91
104
 
92
105
  if (!firstChapter) {
@@ -117,7 +130,13 @@ async function findPlaybackSample (client) {
117
130
  }
118
131
 
119
132
  return {
120
- cardId: contentResponse.card.cardId,
133
+ cardId: contentCard.cardId,
134
+ cardTitle: contentCard.title,
135
+ cardSlug,
136
+ cardCoverImageUrl,
137
+ cardAuthor,
138
+ cardReadBy,
139
+ cardDurationSeconds,
121
140
  tracksByKey,
122
141
  chapterKey: firstChapter.key,
123
142
  chapterTitle: firstChapter.title,
@@ -284,6 +303,12 @@ test('YotoDeviceModel - playback normalization', async (t) => {
284
303
  assert.equal(cachePlayback.trackLength, trackInfo.duration)
285
304
  assert.equal(cachePlayback.chapterKey, trackInfo.chapterKey)
286
305
  assert.equal(cachePlayback.chapterTitle, trackInfo.chapterTitle)
306
+ assert.equal(cachePlayback.cardTitle, sample.cardTitle)
307
+ assert.equal(cachePlayback.cardSlug, sample.cardSlug)
308
+ assert.equal(cachePlayback.cardCoverImageUrl, sample.cardCoverImageUrl)
309
+ assert.equal(cachePlayback.cardAuthor, sample.cardAuthor)
310
+ assert.equal(cachePlayback.cardReadBy, sample.cardReadBy)
311
+ assert.equal(cachePlayback.cardDurationSeconds, sample.cardDurationSeconds)
287
312
  })
288
313
 
289
314
  await t.test('cardId none normalization', async () => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yoto-nodejs-client",
3
3
  "description": "(Unofficial) Node.js client for the Yoto API with automatic token refresh, MQTT device communication, and TypeScript support",
4
- "version": "0.0.9",
4
+ "version": "0.0.11",
5
5
  "author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/bcomnes/yoto-nodejs-client/issues"