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 +1 -1
- package/bin/device-model.js +18 -5
- package/bin/lib/cli-helpers.d.ts.map +1 -1
- package/bin/lib/cli-helpers.js +9 -6
- package/lib/api-endpoints/auth.d.ts.map +1 -1
- package/lib/api-endpoints/auth.js +15 -5
- package/lib/api-endpoints/auth.test.js +3 -3
- package/lib/api-endpoints/content.test.js +1 -1
- package/lib/api-endpoints/devices.test.js +5 -5
- package/lib/api-endpoints/family-library-groups.test.js +1 -1
- package/lib/api-endpoints/family.test.js +2 -2
- package/lib/api-endpoints/helpers.d.ts +5 -3
- package/lib/api-endpoints/helpers.d.ts.map +1 -1
- package/lib/api-endpoints/helpers.js +17 -10
- package/lib/api-endpoints/icons.test.js +2 -2
- package/lib/test-helpers/device-model-test-helpers.d.ts.map +1 -1
- package/lib/test-helpers/device-model-test-helpers.js +6 -0
- package/lib/token.d.ts.map +1 -1
- package/lib/token.js +4 -2
- package/lib/yoto-device.d.ts +42 -2
- package/lib/yoto-device.d.ts.map +1 -1
- package/lib/yoto-device.js +211 -5
- package/lib/yoto-device.test.js +27 -2
- package/package.json +1 -1
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.
|
|
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 {
|
package/bin/device-model.js
CHANGED
|
@@ -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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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,
|
|
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"}
|
package/bin/lib/cli-helpers.js
CHANGED
|
@@ -116,17 +116,20 @@ export function handleCliError (error) {
|
|
|
116
116
|
console.error(`Status Code: ${error.statusCode}`)
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
if (error.
|
|
120
|
-
console.error(`Error: ${error.
|
|
119
|
+
if (error.jsonBody?.error) {
|
|
120
|
+
console.error(`Error: ${error.jsonBody.error}`)
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
if (error.
|
|
124
|
-
console.error(`Description: ${error.
|
|
123
|
+
if (error.jsonBody?.error_description) {
|
|
124
|
+
console.error(`Description: ${error.jsonBody.error_description}`)
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
if (error.
|
|
127
|
+
if (error.jsonBody && typeof error.jsonBody === 'object') {
|
|
128
128
|
console.error('\nFull error response:')
|
|
129
|
-
console.error(JSON.stringify(error.
|
|
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,
|
|
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
|
-
|
|
162
|
-
|
|
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,
|
|
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} */ (
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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,
|
|
53
|
+
constructor(response: Dispatcher.ResponseData, textBody: string, jsonBody: unknown | null, extra?: any);
|
|
53
54
|
/** @type { number } */ statusCode: number;
|
|
54
|
-
/** @type {string
|
|
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,
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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')
|
package/lib/token.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/lib/yoto-device.d.ts
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
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
|
};
|
package/lib/yoto-device.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yoto-device.d.ts","sourceRoot":"","sources":["yoto-device.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"yoto-device.d.ts","sourceRoot":"","sources":["yoto-device.js"],"names":[],"mappings":"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"}
|
package/lib/yoto-device.js
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
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) {
|
package/lib/yoto-device.test.js
CHANGED
|
@@ -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
|
|
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:
|
|
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.
|
|
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"
|