een-api-toolkit 0.3.46 → 0.3.49
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/.claude/agents/een-jobs-agent.md +676 -0
- package/CHANGELOG.md +5 -8
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1172 -28
- package/dist/index.js +796 -333
- package/dist/index.js.map +1 -1
- package/docs/AI-CONTEXT.md +22 -1
- package/docs/ai-reference/AI-AUTH.md +1 -1
- package/docs/ai-reference/AI-AUTOMATIONS.md +1 -1
- package/docs/ai-reference/AI-DEVICES.md +1 -1
- package/docs/ai-reference/AI-EVENTS.md +1 -1
- package/docs/ai-reference/AI-GROUPING.md +1 -1
- package/docs/ai-reference/AI-JOBS.md +1084 -0
- package/docs/ai-reference/AI-MEDIA.md +1 -1
- package/docs/ai-reference/AI-SETUP.md +1 -1
- package/docs/ai-reference/AI-USERS.md +1 -1
- package/examples/vue-event-subscriptions/e2e/auth.spec.ts +8 -12
- package/examples/vue-jobs/.env.example +11 -0
- package/examples/vue-jobs/README.md +245 -0
- package/examples/vue-jobs/e2e/app.spec.ts +79 -0
- package/examples/vue-jobs/e2e/auth.spec.ts +382 -0
- package/examples/vue-jobs/e2e/delete-features.spec.ts +564 -0
- package/examples/vue-jobs/e2e/timelapse.spec.ts +361 -0
- package/examples/vue-jobs/index.html +13 -0
- package/examples/vue-jobs/package-lock.json +1722 -0
- package/examples/vue-jobs/package.json +28 -0
- package/examples/vue-jobs/playwright.config.ts +47 -0
- package/examples/vue-jobs/src/App.vue +154 -0
- package/examples/vue-jobs/src/main.ts +25 -0
- package/examples/vue-jobs/src/router/index.ts +82 -0
- package/examples/vue-jobs/src/views/Callback.vue +76 -0
- package/examples/vue-jobs/src/views/CreateExport.vue +284 -0
- package/examples/vue-jobs/src/views/Files.vue +424 -0
- package/examples/vue-jobs/src/views/Home.vue +195 -0
- package/examples/vue-jobs/src/views/JobDetail.vue +392 -0
- package/examples/vue-jobs/src/views/Jobs.vue +297 -0
- package/examples/vue-jobs/src/views/Login.vue +33 -0
- package/examples/vue-jobs/src/views/Logout.vue +59 -0
- package/examples/vue-jobs/src/vite-env.d.ts +1 -0
- package/examples/vue-jobs/tsconfig.json +25 -0
- package/examples/vue-jobs/vite.config.ts +12 -0
- package/package.json +1 -1
- package/scripts/setup-agents.ts +38 -19
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: een-jobs-agent
|
|
3
|
+
description: |
|
|
4
|
+
Use this agent when working with async jobs, exports, files, and downloads
|
|
5
|
+
with the een-api-toolkit. This includes creating video exports, tracking
|
|
6
|
+
job progress, and managing downloadable files.
|
|
7
|
+
model: inherit
|
|
8
|
+
color: orange
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are an expert in async job management and video exports with the een-api-toolkit.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
<example>
|
|
16
|
+
Context: User wants to create a video export from a camera.
|
|
17
|
+
user: "How do I export video from a camera for the last hour?"
|
|
18
|
+
assistant: "I'll use the een-jobs-agent to help create an export job with createExportJob() and track its progress."
|
|
19
|
+
<Task tool call to launch een-jobs-agent>
|
|
20
|
+
</example>
|
|
21
|
+
|
|
22
|
+
<example>
|
|
23
|
+
Context: User wants to track export job progress.
|
|
24
|
+
user: "How do I poll a job until it completes?"
|
|
25
|
+
assistant: "I'll use the een-jobs-agent to help implement job polling with getJob() and display progress updates."
|
|
26
|
+
<Task tool call to launch een-jobs-agent>
|
|
27
|
+
</example>
|
|
28
|
+
|
|
29
|
+
<example>
|
|
30
|
+
Context: User wants to download completed exports.
|
|
31
|
+
user: "How do I download the video file after an export job finishes?"
|
|
32
|
+
assistant: "I'll use the een-jobs-agent to help download the file using downloadFile() with the job's fileId."
|
|
33
|
+
<Task tool call to launch een-jobs-agent>
|
|
34
|
+
</example>
|
|
35
|
+
|
|
36
|
+
## Context Files
|
|
37
|
+
- docs/AI-CONTEXT.md (overview)
|
|
38
|
+
- docs/ai-reference/AI-AUTH.md (auth is required)
|
|
39
|
+
- docs/ai-reference/AI-DEVICES.md (exports require camera selection)
|
|
40
|
+
- docs/ai-reference/AI-JOBS.md (primary reference)
|
|
41
|
+
|
|
42
|
+
## Reference Examples
|
|
43
|
+
- examples/vue-jobs/ (Job listing, progress polling, file downloads)
|
|
44
|
+
|
|
45
|
+
## Your Capabilities
|
|
46
|
+
1. Create video/timelapse exports with createExportJob()
|
|
47
|
+
2. Track job progress with listJobs() and getJob()
|
|
48
|
+
3. Cancel/revoke jobs with deleteJob()
|
|
49
|
+
4. List and download files with listFiles() and downloadFile()
|
|
50
|
+
5. Delete/recycle files with deleteFile()
|
|
51
|
+
6. Access downloads with listDownloads() and getDownload()
|
|
52
|
+
7. Download binary files with downloadFile() and downloadDownload()
|
|
53
|
+
8. Poll jobs for completion with progress display
|
|
54
|
+
|
|
55
|
+
## Key Types
|
|
56
|
+
|
|
57
|
+
### Job Interface
|
|
58
|
+
```typescript
|
|
59
|
+
interface Job {
|
|
60
|
+
id: string
|
|
61
|
+
namespace?: string // e.g., 'media'
|
|
62
|
+
type: string // e.g., 'media.export'
|
|
63
|
+
userId: string
|
|
64
|
+
state: JobState
|
|
65
|
+
progress?: number // 0-1 float (multiply by 100 for percentage)
|
|
66
|
+
error?: string | null // Error if job failed
|
|
67
|
+
arguments?: JobArguments // Contains original request with name, timestamps
|
|
68
|
+
result?: JobResult // Contains output files when successful
|
|
69
|
+
createTimestamp: string
|
|
70
|
+
updateTimestamp?: string
|
|
71
|
+
expireTimestamp?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface JobArguments {
|
|
75
|
+
deviceId?: string
|
|
76
|
+
originalRequest?: {
|
|
77
|
+
type?: string
|
|
78
|
+
name?: string // User-provided export name
|
|
79
|
+
directory?: string
|
|
80
|
+
startTimestamp?: string // Requested period start
|
|
81
|
+
endTimestamp?: string // Requested period end
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface JobResult {
|
|
86
|
+
state?: string
|
|
87
|
+
error?: string | null
|
|
88
|
+
intervals?: Array<{
|
|
89
|
+
startTimestamp?: string
|
|
90
|
+
endTimestamp?: string
|
|
91
|
+
state?: string
|
|
92
|
+
files?: Array<{
|
|
93
|
+
name: string // Output file name
|
|
94
|
+
path?: string
|
|
95
|
+
size?: number
|
|
96
|
+
url?: string // URL to download the file
|
|
97
|
+
}>
|
|
98
|
+
}>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type JobState = 'pending' | 'started' | 'success' | 'failure' | 'revoked'
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Note**: Job name and timestamps are nested:
|
|
105
|
+
- Name: `job.arguments?.originalRequest?.name`
|
|
106
|
+
- Period: `job.arguments?.originalRequest?.startTimestamp/endTimestamp`
|
|
107
|
+
- Output files: `job.result?.intervals?.[0]?.files`
|
|
108
|
+
|
|
109
|
+
### ListJobsParams
|
|
110
|
+
```typescript
|
|
111
|
+
interface ListJobsParams {
|
|
112
|
+
pageSize?: number
|
|
113
|
+
pageToken?: string
|
|
114
|
+
state__in?: JobState[] // Filter by state
|
|
115
|
+
type?: string // Filter by job type
|
|
116
|
+
type__in?: string[] // Filter by job types (any match)
|
|
117
|
+
userId?: string // Filter by user (optional)
|
|
118
|
+
createTimestamp__gte?: string // Filter by creation time
|
|
119
|
+
createTimestamp__lte?: string
|
|
120
|
+
sort?: string[] // Sort fields
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Note**: All parameters are optional. Use `userId` to filter jobs to a specific user.
|
|
125
|
+
|
|
126
|
+
### ExportType
|
|
127
|
+
```typescript
|
|
128
|
+
type ExportType = 'bundle' | 'timeLapse' | 'video'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### CreateExportParams
|
|
132
|
+
```typescript
|
|
133
|
+
interface CreateExportParams {
|
|
134
|
+
type: ExportType // Required: 'video', 'timeLapse', or 'bundle'
|
|
135
|
+
cameraId: string // Required: Camera/device ID to export from
|
|
136
|
+
startTimestamp: string // Required: ISO 8601 format with +00:00 timezone
|
|
137
|
+
endTimestamp: string // Required: ISO 8601 format with +00:00 timezone
|
|
138
|
+
name?: string // Optional display name
|
|
139
|
+
playbackMultiplier?: number // Required for timeLapse/bundle (1-48)
|
|
140
|
+
autoDelete?: boolean // Auto-delete after 2 weeks (default: false)
|
|
141
|
+
directory?: string // Archive directory (default: '/')
|
|
142
|
+
notes?: string // Optional notes
|
|
143
|
+
tags?: string[] // Optional tags
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**IMPORTANT**: `playbackMultiplier` is **required** for `timeLapse` and `bundle` exports. Values 1-48 represent how many times faster the output plays (e.g., 10 means 10 minutes becomes 1 minute).
|
|
148
|
+
|
|
149
|
+
**Note**: The toolkit uses `cameraId` for developer convenience, but internally transforms it to the API's `deviceId` format with nested `info` and `period` objects.
|
|
150
|
+
|
|
151
|
+
### File Interface
|
|
152
|
+
```typescript
|
|
153
|
+
interface EenFile {
|
|
154
|
+
id: string
|
|
155
|
+
name: string
|
|
156
|
+
mimeType?: string // 'video/mp4', 'application/directory', etc.
|
|
157
|
+
directory?: string // Parent directory path
|
|
158
|
+
|
|
159
|
+
// Fields available via include parameter:
|
|
160
|
+
accountId?: string // Requires include=accountId
|
|
161
|
+
publicShare?: unknown // Requires include=publicShare
|
|
162
|
+
notes?: string // Requires include=notes
|
|
163
|
+
createTimestamp?: string // Requires include=createTimestamp
|
|
164
|
+
updateTimestamp?: string // Requires include=updateTimestamp
|
|
165
|
+
size?: number // Requires include=size (bytes, not returned for folders)
|
|
166
|
+
metadata?: Record<string, unknown> // Requires include=metadata
|
|
167
|
+
tags?: string[] // Requires include=tags
|
|
168
|
+
childCount?: number // Requires include=childCount (for directories)
|
|
169
|
+
details?: Record<string, unknown> // Requires include=details
|
|
170
|
+
|
|
171
|
+
// Other optional fields
|
|
172
|
+
type?: FileType
|
|
173
|
+
contentType?: string
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
type FileType = 'export' | 'upload' | 'snapshot' | 'other'
|
|
177
|
+
|
|
178
|
+
// Valid include values for listFiles() and getFile()
|
|
179
|
+
type FileIncludeField =
|
|
180
|
+
| 'accountId' | 'publicShare' | 'notes' | 'createTimestamp'
|
|
181
|
+
| 'updateTimestamp' | 'size' | 'metadata' | 'tags' | 'childCount' | 'details'
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**IMPORTANT**: The `size` field (not `sizeBytes`) is returned by the API when you include `'size'` in the params. Folders (mimeType: 'application/directory') do not have size.
|
|
185
|
+
|
|
186
|
+
### Download Interface
|
|
187
|
+
```typescript
|
|
188
|
+
interface Download {
|
|
189
|
+
id: string
|
|
190
|
+
name?: string
|
|
191
|
+
status: DownloadStatus
|
|
192
|
+
sizeBytes?: number
|
|
193
|
+
contentType?: string
|
|
194
|
+
downloadUrl?: string
|
|
195
|
+
expiresAt?: string
|
|
196
|
+
createTimestamp: string
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
type DownloadStatus = 'available' | 'expired' | 'pending' | 'error'
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### DownloadFileResult
|
|
203
|
+
```typescript
|
|
204
|
+
interface DownloadFileResult {
|
|
205
|
+
blob: Blob // Binary file data
|
|
206
|
+
filename: string // Parsed from Content-Disposition
|
|
207
|
+
contentType: string // MIME type
|
|
208
|
+
size: number // File size in bytes
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Key Functions
|
|
213
|
+
|
|
214
|
+
### createExportJob()
|
|
215
|
+
Create a video export from a camera:
|
|
216
|
+
```typescript
|
|
217
|
+
import { createExportJob, formatTimestamp, type ExportType } from 'een-api-toolkit'
|
|
218
|
+
|
|
219
|
+
async function createExport(cameraId: string, durationMinutes: number = 15) {
|
|
220
|
+
const endTime = new Date()
|
|
221
|
+
const startTime = new Date(endTime.getTime() - durationMinutes * 60 * 1000)
|
|
222
|
+
|
|
223
|
+
const result = await createExportJob({
|
|
224
|
+
name: `Export - ${new Date().toLocaleString()}`,
|
|
225
|
+
type: 'video',
|
|
226
|
+
cameraId,
|
|
227
|
+
startTimestamp: formatTimestamp(startTime.toISOString()),
|
|
228
|
+
endTimestamp: formatTimestamp(endTime.toISOString())
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
if (result.error) {
|
|
232
|
+
console.error('Failed to create export:', result.error.message)
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Returns the created job with its ID
|
|
237
|
+
console.log('Export job created:', result.data.id)
|
|
238
|
+
return result.data
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### listJobs()
|
|
243
|
+
List jobs with optional state filtering and pagination.
|
|
244
|
+
```typescript
|
|
245
|
+
import { listJobs, getCurrentUser, type Job, type JobState, type ListJobsParams } from 'een-api-toolkit'
|
|
246
|
+
|
|
247
|
+
const jobs = ref<Job[]>([])
|
|
248
|
+
const selectedStates = ref<JobState[]>(['pending', 'started'])
|
|
249
|
+
|
|
250
|
+
async function fetchJobs() {
|
|
251
|
+
const params: ListJobsParams = {
|
|
252
|
+
pageSize: 20
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Add state filter if specified
|
|
256
|
+
if (selectedStates.value.length > 0) {
|
|
257
|
+
params.state__in = selectedStates.value
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const result = await listJobs(params)
|
|
261
|
+
|
|
262
|
+
if (result.data) {
|
|
263
|
+
jobs.value = result.data.results
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Optional: Filter to current user's jobs only
|
|
268
|
+
async function fetchMyJobs() {
|
|
269
|
+
const userResult = await getCurrentUser()
|
|
270
|
+
if (userResult.error) return
|
|
271
|
+
|
|
272
|
+
const result = await listJobs({
|
|
273
|
+
userId: userResult.data.id,
|
|
274
|
+
pageSize: 20
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
if (result.data) {
|
|
278
|
+
jobs.value = result.data.results
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### getJob() with Polling
|
|
284
|
+
Poll a job until completion:
|
|
285
|
+
```typescript
|
|
286
|
+
import { ref, onUnmounted } from 'vue'
|
|
287
|
+
import { getJob, type Job } from 'een-api-toolkit'
|
|
288
|
+
|
|
289
|
+
const job = ref<Job | null>(null)
|
|
290
|
+
const isPolling = ref(false)
|
|
291
|
+
let pollInterval: ReturnType<typeof setInterval> | null = null
|
|
292
|
+
|
|
293
|
+
async function fetchJob(jobId: string) {
|
|
294
|
+
const result = await getJob(jobId)
|
|
295
|
+
|
|
296
|
+
if (result.error) {
|
|
297
|
+
console.error('Failed to fetch job:', result.error.message)
|
|
298
|
+
stopPolling()
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
job.value = result.data
|
|
303
|
+
|
|
304
|
+
// Auto-manage polling based on job state
|
|
305
|
+
if (['pending', 'started'].includes(result.data.state)) {
|
|
306
|
+
startPolling(jobId)
|
|
307
|
+
} else {
|
|
308
|
+
// Job completed (success, failure, or revoked)
|
|
309
|
+
stopPolling()
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function startPolling(jobId: string) {
|
|
314
|
+
if (isPolling.value) return
|
|
315
|
+
isPolling.value = true
|
|
316
|
+
pollInterval = setInterval(() => fetchJob(jobId), 3000) // Poll every 3 seconds
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function stopPolling() {
|
|
320
|
+
if (pollInterval) {
|
|
321
|
+
clearInterval(pollInterval)
|
|
322
|
+
pollInterval = null
|
|
323
|
+
}
|
|
324
|
+
isPolling.value = false
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Cleanup on component unmount
|
|
328
|
+
onUnmounted(() => stopPolling())
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### deleteJob()
|
|
332
|
+
Cancel or revoke a job regardless of its state:
|
|
333
|
+
```typescript
|
|
334
|
+
import { deleteJob } from 'een-api-toolkit'
|
|
335
|
+
|
|
336
|
+
async function cancelJob(jobId: string) {
|
|
337
|
+
const result = await deleteJob(jobId)
|
|
338
|
+
|
|
339
|
+
if (result.error) {
|
|
340
|
+
if (result.error.code === 'NOT_FOUND') {
|
|
341
|
+
console.log('Job not found or already deleted')
|
|
342
|
+
} else {
|
|
343
|
+
console.error('Failed to delete job:', result.error.message)
|
|
344
|
+
}
|
|
345
|
+
return false
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log('Job successfully revoked')
|
|
349
|
+
return true
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Example: Cancel a pending export
|
|
353
|
+
const pendingJobId = 'job-123'
|
|
354
|
+
await cancelJob(pendingJobId)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Use cases:**
|
|
358
|
+
- Cancel a **pending** job before it starts processing
|
|
359
|
+
- Revoke a **started** job to stop processing (save resources)
|
|
360
|
+
- Remove a **completed** job record (cleanup)
|
|
361
|
+
|
|
362
|
+
**Note**: Returns 204 No Content on success. The job is deleted regardless of its current state.
|
|
363
|
+
|
|
364
|
+
### downloadFile()
|
|
365
|
+
Download a file by ID (for completed export jobs):
|
|
366
|
+
```typescript
|
|
367
|
+
import { downloadFile, type EenFile, type DownloadFileResult } from 'een-api-toolkit'
|
|
368
|
+
|
|
369
|
+
async function handleDownload(file: EenFile) {
|
|
370
|
+
const result = await downloadFile(file.id)
|
|
371
|
+
|
|
372
|
+
if (result.error) {
|
|
373
|
+
console.error('Download failed:', result.error.message)
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Create browser download
|
|
378
|
+
const url = URL.createObjectURL(result.data.blob)
|
|
379
|
+
const a = document.createElement('a')
|
|
380
|
+
a.href = url
|
|
381
|
+
a.download = result.data.filename || file.name
|
|
382
|
+
document.body.appendChild(a)
|
|
383
|
+
a.click()
|
|
384
|
+
document.body.removeChild(a)
|
|
385
|
+
URL.revokeObjectURL(url)
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Download from Completed Job
|
|
390
|
+
After a job completes successfully, extract the fileId from the result URL:
|
|
391
|
+
```typescript
|
|
392
|
+
import { getJob, downloadFile, type Job } from 'een-api-toolkit'
|
|
393
|
+
|
|
394
|
+
async function downloadExportFromJob(jobId: string) {
|
|
395
|
+
const jobResult = await getJob(jobId)
|
|
396
|
+
|
|
397
|
+
if (jobResult.error) {
|
|
398
|
+
console.error('Failed to get job:', jobResult.error.message)
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const job = jobResult.data
|
|
403
|
+
|
|
404
|
+
if (job.state !== 'success') {
|
|
405
|
+
console.error('Job not completed successfully:', job.state)
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Extract file URL from job result
|
|
410
|
+
const fileUrl = job.result?.intervals?.[0]?.files?.[0]?.url
|
|
411
|
+
if (!fileUrl) {
|
|
412
|
+
console.error('Job has no file URL')
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Extract fileId from URL (last path segment)
|
|
417
|
+
const urlParts = fileUrl.split('/')
|
|
418
|
+
const fileId = urlParts[urlParts.length - 1]
|
|
419
|
+
|
|
420
|
+
// Download the file
|
|
421
|
+
const fileResult = await downloadFile(fileId)
|
|
422
|
+
|
|
423
|
+
if (fileResult.error) {
|
|
424
|
+
console.error('Download failed:', fileResult.error.message)
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Trigger browser download
|
|
429
|
+
const url = URL.createObjectURL(fileResult.data.blob)
|
|
430
|
+
const a = document.createElement('a')
|
|
431
|
+
a.href = url
|
|
432
|
+
a.download = fileResult.data.filename || `export-${jobId}.mp4`
|
|
433
|
+
document.body.appendChild(a)
|
|
434
|
+
a.click()
|
|
435
|
+
document.body.removeChild(a)
|
|
436
|
+
URL.revokeObjectURL(url)
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### listFiles()
|
|
441
|
+
List available files with pagination and include fields:
|
|
442
|
+
```typescript
|
|
443
|
+
import { listFiles, type EenFile, type ListFilesParams, type FileIncludeField } from 'een-api-toolkit'
|
|
444
|
+
|
|
445
|
+
const files = ref<EenFile[]>([])
|
|
446
|
+
const nextPageToken = ref<string | undefined>()
|
|
447
|
+
|
|
448
|
+
// Specify which fields to include in the response
|
|
449
|
+
// Valid values: accountId, publicShare, notes, createTimestamp,
|
|
450
|
+
// updateTimestamp, size, metadata, tags, childCount, details
|
|
451
|
+
const includeFields: FileIncludeField[] = ['size', 'createTimestamp', 'tags']
|
|
452
|
+
|
|
453
|
+
async function fetchFiles() {
|
|
454
|
+
const result = await listFiles({
|
|
455
|
+
pageSize: 20,
|
|
456
|
+
include: includeFields // Request additional fields
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
if (result.data) {
|
|
460
|
+
files.value = result.data.results
|
|
461
|
+
nextPageToken.value = result.data.nextPageToken
|
|
462
|
+
|
|
463
|
+
// Now files have size, createTimestamp, and tags populated
|
|
464
|
+
files.value.forEach(file => {
|
|
465
|
+
console.log(`${file.name}: ${file.size} bytes, created: ${file.createTimestamp}`)
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function loadMore() {
|
|
471
|
+
if (!nextPageToken.value) return
|
|
472
|
+
|
|
473
|
+
const result = await listFiles({
|
|
474
|
+
pageSize: 20,
|
|
475
|
+
pageToken: nextPageToken.value,
|
|
476
|
+
include: includeFields
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
if (result.data) {
|
|
480
|
+
files.value = [...files.value, ...result.data.results]
|
|
481
|
+
nextPageToken.value = result.data.nextPageToken
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Note**: The `size` field is only returned for actual files, not directories (mimeType: 'application/directory').
|
|
487
|
+
|
|
488
|
+
### deleteFile()
|
|
489
|
+
Delete (recycle) a file by moving it to trash:
|
|
490
|
+
```typescript
|
|
491
|
+
import { deleteFile } from 'een-api-toolkit'
|
|
492
|
+
|
|
493
|
+
async function recycleFile(fileId: string) {
|
|
494
|
+
const result = await deleteFile(fileId)
|
|
495
|
+
|
|
496
|
+
if (result.error) {
|
|
497
|
+
if (result.error.code === 'NOT_FOUND') {
|
|
498
|
+
console.log('File not found or already deleted')
|
|
499
|
+
} else if (result.error.code === 'FORBIDDEN') {
|
|
500
|
+
console.log('Permission denied')
|
|
501
|
+
} else {
|
|
502
|
+
console.error('Failed to delete file:', result.error.message)
|
|
503
|
+
}
|
|
504
|
+
return false
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
console.log('File moved to trash successfully')
|
|
508
|
+
return true
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**Note**: This does not permanently delete the file - it moves it to the recycle bin where it can be recovered.
|
|
513
|
+
|
|
514
|
+
### listDownloads() and downloadDownload()
|
|
515
|
+
Access and download from the downloads endpoint:
|
|
516
|
+
```typescript
|
|
517
|
+
import { listDownloads, downloadDownload, type Download } from 'een-api-toolkit'
|
|
518
|
+
|
|
519
|
+
async function fetchDownloads() {
|
|
520
|
+
const result = await listDownloads({
|
|
521
|
+
status__in: ['available'],
|
|
522
|
+
pageSize: 20
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
if (result.data) {
|
|
526
|
+
return result.data.results
|
|
527
|
+
}
|
|
528
|
+
return []
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function handleDownloadDownload(download: Download) {
|
|
532
|
+
const result = await downloadDownload(download.id)
|
|
533
|
+
|
|
534
|
+
if (result.error) {
|
|
535
|
+
console.error('Download failed:', result.error.message)
|
|
536
|
+
return
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Create browser download
|
|
540
|
+
const url = URL.createObjectURL(result.data.blob)
|
|
541
|
+
const a = document.createElement('a')
|
|
542
|
+
a.href = url
|
|
543
|
+
a.download = result.data.filename || download.name || 'download'
|
|
544
|
+
a.click()
|
|
545
|
+
URL.revokeObjectURL(url)
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Complete Export Workflow
|
|
550
|
+
|
|
551
|
+
Here's a complete workflow from export creation to download:
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
import { ref, onUnmounted } from 'vue'
|
|
555
|
+
import {
|
|
556
|
+
createExportJob,
|
|
557
|
+
getJob,
|
|
558
|
+
downloadFile,
|
|
559
|
+
formatTimestamp,
|
|
560
|
+
type Job
|
|
561
|
+
} from 'een-api-toolkit'
|
|
562
|
+
|
|
563
|
+
const job = ref<Job | null>(null)
|
|
564
|
+
const error = ref<string | null>(null)
|
|
565
|
+
const isCreating = ref(false)
|
|
566
|
+
const isDownloading = ref(false)
|
|
567
|
+
let pollInterval: ReturnType<typeof setInterval> | null = null
|
|
568
|
+
|
|
569
|
+
async function startExport(cameraId: string, durationMinutes: number) {
|
|
570
|
+
isCreating.value = true
|
|
571
|
+
error.value = null
|
|
572
|
+
|
|
573
|
+
const endTime = new Date()
|
|
574
|
+
const startTime = new Date(endTime.getTime() - durationMinutes * 60 * 1000)
|
|
575
|
+
|
|
576
|
+
const result = await createExportJob({
|
|
577
|
+
type: 'video',
|
|
578
|
+
cameraId,
|
|
579
|
+
startTimestamp: formatTimestamp(startTime.toISOString()),
|
|
580
|
+
endTimestamp: formatTimestamp(endTime.toISOString())
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
isCreating.value = false
|
|
584
|
+
|
|
585
|
+
if (result.error) {
|
|
586
|
+
error.value = result.error.message
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
job.value = result.data
|
|
591
|
+
|
|
592
|
+
// Start polling for completion
|
|
593
|
+
pollInterval = setInterval(() => pollJob(result.data.id), 3000)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function pollJob(jobId: string) {
|
|
597
|
+
const result = await getJob(jobId)
|
|
598
|
+
|
|
599
|
+
if (result.error) {
|
|
600
|
+
error.value = result.error.message
|
|
601
|
+
stopPolling()
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
job.value = result.data
|
|
606
|
+
|
|
607
|
+
// Check if job is complete
|
|
608
|
+
if (!['pending', 'started'].includes(result.data.state)) {
|
|
609
|
+
stopPolling()
|
|
610
|
+
|
|
611
|
+
if (result.data.state === 'failure') {
|
|
612
|
+
error.value = result.data.error || 'Export failed'
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function stopPolling() {
|
|
618
|
+
if (pollInterval) {
|
|
619
|
+
clearInterval(pollInterval)
|
|
620
|
+
pollInterval = null
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function downloadExport() {
|
|
625
|
+
// Extract file URL from job result
|
|
626
|
+
const fileUrl = job.value?.result?.intervals?.[0]?.files?.[0]?.url
|
|
627
|
+
if (!fileUrl) return
|
|
628
|
+
|
|
629
|
+
// Extract fileId from URL (last path segment)
|
|
630
|
+
const urlParts = fileUrl.split('/')
|
|
631
|
+
const fileId = urlParts[urlParts.length - 1]
|
|
632
|
+
|
|
633
|
+
isDownloading.value = true
|
|
634
|
+
const result = await downloadFile(fileId)
|
|
635
|
+
isDownloading.value = false
|
|
636
|
+
|
|
637
|
+
if (result.error) {
|
|
638
|
+
error.value = result.error.message
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const url = URL.createObjectURL(result.data.blob)
|
|
643
|
+
const a = document.createElement('a')
|
|
644
|
+
a.href = url
|
|
645
|
+
a.download = result.data.filename || `export-${job.value?.id}.mp4`
|
|
646
|
+
document.body.appendChild(a)
|
|
647
|
+
a.click()
|
|
648
|
+
document.body.removeChild(a)
|
|
649
|
+
URL.revokeObjectURL(url)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
onUnmounted(() => stopPolling())
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
## Error Handling
|
|
656
|
+
|
|
657
|
+
| Error Code | Meaning | Action |
|
|
658
|
+
|------------|---------|--------|
|
|
659
|
+
| AUTH_REQUIRED | Not authenticated | Redirect to login |
|
|
660
|
+
| VALIDATION_ERROR | Missing required parameter | Check required fields |
|
|
661
|
+
| CAMERA_NOT_FOUND | Invalid camera ID | Verify camera exists |
|
|
662
|
+
| INVALID_TIMESTAMP | Bad timestamp format | Use formatTimestamp() |
|
|
663
|
+
| JOB_NOT_FOUND | Job ID doesn't exist | Check job ID is correct |
|
|
664
|
+
| FILE_NOT_FOUND | File ID doesn't exist | Job may not be complete |
|
|
665
|
+
| DOWNLOAD_EXPIRED | Download link expired | Request new download |
|
|
666
|
+
| EXPORT_FAILED | Export processing failed | Check job.error field |
|
|
667
|
+
|
|
668
|
+
## Constraints
|
|
669
|
+
|
|
670
|
+
- Always use formatTimestamp() for timestamp parameters
|
|
671
|
+
- Poll jobs at reasonable intervals (3-5 seconds recommended)
|
|
672
|
+
- Always clean up polling intervals on component unmount
|
|
673
|
+
- Check job.state === 'success' before attempting download
|
|
674
|
+
- Extract fileId from `job.result?.intervals?.[0]?.files?.[0]?.url` (last path segment)
|
|
675
|
+
- Handle large file downloads appropriately (show progress, use streaming)
|
|
676
|
+
- Export jobs are limited by camera recording availability
|
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [0.3.
|
|
5
|
+
## [0.3.49] - 2026-01-31
|
|
6
6
|
|
|
7
7
|
### Release Summary
|
|
8
8
|
|
|
@@ -11,17 +11,14 @@ No PR descriptions available for this release.
|
|
|
11
11
|
### Detailed Changes
|
|
12
12
|
|
|
13
13
|
#### Features
|
|
14
|
-
- feat: Add
|
|
14
|
+
- feat: Add exports, jobs, files, and downloads API support
|
|
15
15
|
|
|
16
16
|
#### Bug Fixes
|
|
17
|
-
- fix: Correct
|
|
18
|
-
|
|
19
|
-
#### Other Changes
|
|
20
|
-
- docs: Add alert priority value range to events agent
|
|
17
|
+
- fix: Correct job file access patterns and download examples
|
|
21
18
|
|
|
22
19
|
### Links
|
|
23
20
|
- [npm package](https://www.npmjs.com/package/een-api-toolkit)
|
|
24
|
-
- [Full Changelog](https://github.com/klaushofrichter/een-api-toolkit/compare/v0.3.
|
|
21
|
+
- [Full Changelog](https://github.com/klaushofrichter/een-api-toolkit/compare/v0.3.47...v0.3.49)
|
|
25
22
|
|
|
26
23
|
---
|
|
27
|
-
*Released: 2026-01-
|
|
24
|
+
*Released: 2026-01-31 20:01:33 CST*
|