@mostajs/media-server 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +349 -0
- package/dist/factory.d.ts +53 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +175 -0
- package/dist/factory.js.map +1 -0
- package/dist/handlers/next.d.ts +71 -0
- package/dist/handlers/next.d.ts.map +1 -0
- package/dist/handlers/next.js +189 -0
- package/dist/handlers/next.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.d.ts +98 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +9 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/docs/RATIONALE-19052026.md +31 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# @mostajs/media-server
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
**Licence** : AGPL-3.0-or-later
|
|
5
|
+
**Statut** : v0.1.0 — scaffold + factory + contract (P1 du design B.4)
|
|
6
|
+
|
|
7
|
+
Couche **serveur** du module [@mostajs/media](../mosta-media) : factory
|
|
8
|
+
`createMedia({ storage, repo })` qui pluge un repository ORM sur un driver
|
|
9
|
+
[@mostajs/storage](https://github.com/apolocine/mosta-storage) pour livrer un
|
|
10
|
+
**`MediaService`** consommable par les route handlers HTTP (Next.js App
|
|
11
|
+
Router, Express, Fastify, Hono, …).
|
|
12
|
+
|
|
13
|
+
> 🧩 Ce module est **strictement server-side**. Le client utilise
|
|
14
|
+
> `@mostajs/media` (composants React, hooks `useMultiTakeSession`, etc.) et
|
|
15
|
+
> appelle ce serveur via fetch.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Pourquoi un module séparé ?
|
|
20
|
+
|
|
21
|
+
`@mostajs/media` est ESM **browser-first** (React, MediaRecorder, IndexedDB).
|
|
22
|
+
Y empiler la persistance serveur (ORM, mailer, file I/O) :
|
|
23
|
+
|
|
24
|
+
- alourdit le bundle client d'environ 800 kB de dépendances inutiles ;
|
|
25
|
+
- complique le tree-shaking pour les apps Next.js / Vite ;
|
|
26
|
+
- mélange deux cycles de vie (browser API vs Node API) dans un même release.
|
|
27
|
+
|
|
28
|
+
`@mostajs/media-server` reste **indépendant** : pas de dep React, pas de
|
|
29
|
+
MediaRecorder, pas de `@mostajs/orm` en hard-dep (peer dep seulement).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Architecture en 1 schéma
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
┌─────────────────────────┐ POST /api/media/recordings ┌────────────────────────┐
|
|
37
|
+
│ Browser │ ───────────────────────────────▶ │ Route handler │
|
|
38
|
+
│ @mostajs/media │ │ (Next.js App Router, │
|
|
39
|
+
│ MultiTakeRecorder │ │ Express, Fastify…) │
|
|
40
|
+
│ videoStorage='server' │ ◀────────── 200 { row, signedUrl}│ │
|
|
41
|
+
└─────────────────────────┘ │ • RBAC accountId │
|
|
42
|
+
│ • parse multipart │
|
|
43
|
+
│ • mediaService.…(…) │
|
|
44
|
+
└────────────┬───────────┘
|
|
45
|
+
│
|
|
46
|
+
┌──────────────┼──────────────┐
|
|
47
|
+
▼ ▼
|
|
48
|
+
┌───────────────────┐ ┌──────────────────┐
|
|
49
|
+
│ MediaRepository │ │ @mostajs/storage │
|
|
50
|
+
│ (ORM, Prisma, …) │ │ FileStore │
|
|
51
|
+
│ → row metadata │ │ → blob bytes │
|
|
52
|
+
└───────────────────┘ └──────────────────┘
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install @mostajs/media-server @mostajs/storage
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Peer deps requises : `@mostajs/storage ≥ 0.1.2`, `@mostajs/media ≥ 2.0.4`
|
|
64
|
+
(pour le contract `recordingMode`).
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## How to use — 5 étapes
|
|
69
|
+
|
|
70
|
+
### 1. Préparer le storage (driver FS local pour la démo)
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// lib/storage.ts
|
|
74
|
+
import { FilesystemDriver, createFileStore } from '@mostajs/storage'
|
|
75
|
+
|
|
76
|
+
const driver = new FilesystemDriver({
|
|
77
|
+
rootDir: '/home/hmd/storage/myapp',
|
|
78
|
+
signedUrlSecret: process.env.STORAGE_SIGNING_SECRET!,
|
|
79
|
+
signedUrlBaseUrl: 'https://myapp.example.com',
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
export const storage = createFileStore({ driver })
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2. Implémenter le `MediaRepository` (au-dessus de votre ORM)
|
|
86
|
+
|
|
87
|
+
Le repo est la **seule** surface ORM exposée — `@mostajs/media-server` ne
|
|
88
|
+
voit ni Prisma, ni @mostajs/orm, ni Drizzle. Vous le câblez à votre data layer.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// lib/media-repo.ts
|
|
92
|
+
import type { MediaRepository, MediaRow, MediaListFilter } from '@mostajs/media-server'
|
|
93
|
+
import { db } from './db' // votre client ORM
|
|
94
|
+
|
|
95
|
+
export const mediaRepo: MediaRepository = {
|
|
96
|
+
async insert(data) {
|
|
97
|
+
const id = data.id ?? crypto.randomUUID()
|
|
98
|
+
const row: MediaRow = {
|
|
99
|
+
...data,
|
|
100
|
+
id,
|
|
101
|
+
createdAt: new Date(),
|
|
102
|
+
}
|
|
103
|
+
await db.media.create({ data: row })
|
|
104
|
+
return row
|
|
105
|
+
},
|
|
106
|
+
async findById(id) {
|
|
107
|
+
return db.media.findUnique({ where: { id } })
|
|
108
|
+
},
|
|
109
|
+
async update(id, patch) {
|
|
110
|
+
return db.media.update({ where: { id }, data: { ...patch, updatedAt: new Date() } })
|
|
111
|
+
},
|
|
112
|
+
async delete(id) {
|
|
113
|
+
await db.media.delete({ where: { id } })
|
|
114
|
+
},
|
|
115
|
+
async list(accountId, filter) {
|
|
116
|
+
const status = filter?.status
|
|
117
|
+
? (Array.isArray(filter.status) ? filter.status : [filter.status])
|
|
118
|
+
: ['ready']
|
|
119
|
+
const rows = await db.media.findMany({
|
|
120
|
+
where: { accountId, status: { in: status } },
|
|
121
|
+
orderBy: { createdAt: 'desc' },
|
|
122
|
+
take: Math.min(filter?.limit ?? 50, 200),
|
|
123
|
+
})
|
|
124
|
+
return { items: rows }
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
> 💡 **Tip RBAC** : ne JAMAIS filtrer par `userId`. Toujours par `accountId`,
|
|
130
|
+
> conformément à la mémoire `feedback_account_id_vs_user_id` du workspace.
|
|
131
|
+
|
|
132
|
+
### 3. Instancier le service
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
// lib/media-service.ts
|
|
136
|
+
import { createMedia } from '@mostajs/media-server'
|
|
137
|
+
import { storage } from './storage'
|
|
138
|
+
import { mediaRepo } from './media-repo'
|
|
139
|
+
|
|
140
|
+
export const mediaService = createMedia({
|
|
141
|
+
storage,
|
|
142
|
+
repo: mediaRepo,
|
|
143
|
+
defaultBucket: 'media-recordings',
|
|
144
|
+
signedUrlTtlSec: 60 * 60, // 1h
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 4. Câbler les routes HTTP
|
|
149
|
+
|
|
150
|
+
#### Next.js App Router — `POST /api/media/recordings`
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// app/api/media/recordings/route.ts
|
|
154
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
155
|
+
import { mediaService } from '@/lib/media-service'
|
|
156
|
+
import { getAccountIdFromSession } from '@/lib/session' // votre helper auth
|
|
157
|
+
|
|
158
|
+
export const runtime = 'nodejs'
|
|
159
|
+
|
|
160
|
+
export async function POST(req: NextRequest) {
|
|
161
|
+
const accountId = await getAccountIdFromSession(req)
|
|
162
|
+
if (!accountId) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
|
|
163
|
+
|
|
164
|
+
const form = await req.formData()
|
|
165
|
+
const blob = form.get('blob') as Blob
|
|
166
|
+
const mimeType = (form.get('mimeType') as string) || blob.type
|
|
167
|
+
const durationMs = Number(form.get('durationMs') || 0)
|
|
168
|
+
const sessionId = form.get('sessionId') as string | null
|
|
169
|
+
const takeIndex = form.get('takeIndex') ? Number(form.get('takeIndex')) : undefined
|
|
170
|
+
|
|
171
|
+
const buf = new Uint8Array(await blob.arrayBuffer())
|
|
172
|
+
const { row, signedUrl, signedUrlExpiresAt } = await mediaService.createRecording({
|
|
173
|
+
accountId,
|
|
174
|
+
body: buf,
|
|
175
|
+
mimeType,
|
|
176
|
+
durationMs,
|
|
177
|
+
sessionId: sessionId ?? undefined,
|
|
178
|
+
takeIndex,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
return NextResponse.json({ row, signedUrl, signedUrlExpiresAt }, { status: 201 })
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### Next.js App Router — `GET /api/media/[id]`
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// app/api/media/[id]/route.ts
|
|
189
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
190
|
+
import { mediaService } from '@/lib/media-service'
|
|
191
|
+
import { getAccountIdFromSession } from '@/lib/session'
|
|
192
|
+
|
|
193
|
+
export const runtime = 'nodejs'
|
|
194
|
+
|
|
195
|
+
export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
|
|
196
|
+
const accountId = await getAccountIdFromSession(req)
|
|
197
|
+
if (!accountId) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
|
|
198
|
+
|
|
199
|
+
const { id } = await ctx.params
|
|
200
|
+
const result = await mediaService.getMedia(id)
|
|
201
|
+
if (!result) return NextResponse.json({ error: 'not_found' }, { status: 404 })
|
|
202
|
+
|
|
203
|
+
// RBAC : protection tenant — la row doit appartenir à l'account de la session
|
|
204
|
+
if (result.row.accountId !== accountId) {
|
|
205
|
+
return NextResponse.json({ error: 'forbidden' }, { status: 403 })
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return NextResponse.json(result)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function DELETE(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
|
|
212
|
+
const accountId = await getAccountIdFromSession(req)
|
|
213
|
+
if (!accountId) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
|
|
214
|
+
|
|
215
|
+
const { id } = await ctx.params
|
|
216
|
+
const existing = await mediaService.getMedia(id)
|
|
217
|
+
if (existing && existing.row.accountId !== accountId) {
|
|
218
|
+
return NextResponse.json({ error: 'forbidden' }, { status: 403 })
|
|
219
|
+
}
|
|
220
|
+
await mediaService.deleteMedia(id)
|
|
221
|
+
return NextResponse.json({ ok: true })
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 5. Câbler `@mostajs/media` côté browser
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
// app/page.tsx (camera-studio ou consumer custom)
|
|
229
|
+
'use client'
|
|
230
|
+
import { MultiTakeRecorder } from '@mostajs/media'
|
|
231
|
+
|
|
232
|
+
export default function Page() {
|
|
233
|
+
return (
|
|
234
|
+
<MultiTakeRecorder
|
|
235
|
+
initialVideoStorage="server"
|
|
236
|
+
initialVideoServerUrl="/api/media/recordings"
|
|
237
|
+
onSessionComplete={async (takes, sessionId, meta) => {
|
|
238
|
+
// Les takes sont déjà POSTés en chunks pendant l'enregistrement
|
|
239
|
+
// (storage='server'). Ici on peut lister/recharger via getMedia.
|
|
240
|
+
for (const take of takes) {
|
|
241
|
+
// take.videoResult.serverUrl pointera vers la row Media créée
|
|
242
|
+
console.log('Take saved:', take.id, take.videoResult.storage)
|
|
243
|
+
}
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## API détaillée
|
|
253
|
+
|
|
254
|
+
### `createMedia(opts: CreateMediaOptions): MediaService`
|
|
255
|
+
|
|
256
|
+
| Option | Type | Défaut | Notes |
|
|
257
|
+
|---------------------|-----------------|---------------------|-------|
|
|
258
|
+
| `storage` | `StorageLike` | — | FileStore @mostajs/storage |
|
|
259
|
+
| `repo` | `MediaRepository` | — | Votre repo ORM |
|
|
260
|
+
| `defaultBucket` | `string` | `'media-recordings'` | Bucket cible des uploads |
|
|
261
|
+
| `signedUrlTtlSec` | `number` | `3600` (1h) | TTL signed URLs |
|
|
262
|
+
| `generateId` | `() => string` | base36 timestamp | Surcharger pour UUID/ULID |
|
|
263
|
+
|
|
264
|
+
### `MediaService`
|
|
265
|
+
|
|
266
|
+
| Méthode | Signature | Effet |
|
|
267
|
+
|---------|-----------|-------|
|
|
268
|
+
| `createRecording` | `(input) => Promise<{ row, signedUrl, signedUrlExpiresAt }>` | insert row(uploading) → put storage → update row(ready) → sign URL. Si put fail → row.status=failed |
|
|
269
|
+
| `getMedia` | `(id) => Promise<MediaWithSignedUrl \| null>` | Récupère row + signedUrl frais. Renvoie null si status ≠ ready |
|
|
270
|
+
| `reissueSignedUrl` | `(id, ttlSec?) => Promise<MediaWithSignedUrl \| null>` | Re-sign sans toucher la row |
|
|
271
|
+
| `deleteMedia` | `(id) => Promise<void>` | Soft-delete row (status='deleted') puis purge storage |
|
|
272
|
+
| `listForAccount` | `(accountId, filter?) => Promise<{ items, nextCursor? }>` | Pagination multi-tenant |
|
|
273
|
+
|
|
274
|
+
### `MediaRow` (row métadonnée)
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
{
|
|
278
|
+
id, accountId, ownerEmail?,
|
|
279
|
+
bucket, key,
|
|
280
|
+
mimeType, sizeBytes, durationMs, kind,
|
|
281
|
+
status: 'uploading' | 'ready' | 'deleted' | 'failed',
|
|
282
|
+
sessionId?, takeIndex?,
|
|
283
|
+
checksum?, metadata?,
|
|
284
|
+
createdAt, updatedAt?,
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Lifecycle row Media
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
[POST] ──▶ insert row(uploading) ──▶ storage.put() ──┬─▶ update row(ready) ──▶ signedUrl ──▶ 201
|
|
294
|
+
│
|
|
295
|
+
└─▶ (failure) update row(failed) ──▶ throw
|
|
296
|
+
|
|
297
|
+
[DELETE] ──▶ update row(deleted) ──▶ storage.delete() (best-effort, ne re-throw pas si fail)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Tests & smoke
|
|
303
|
+
|
|
304
|
+
Pas de tests dans v0.1.0 — la livraison du **2e cas concret** (iquesta
|
|
305
|
+
course-builder, après camera-studio) déclenchera la suite Vitest minimale
|
|
306
|
+
(mock storage + mock repo). Référence mémoire workspace :
|
|
307
|
+
`project_deployment_validation_modules`.
|
|
308
|
+
|
|
309
|
+
Smoke local rapide :
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
// test-smoke.ts (ad-hoc)
|
|
313
|
+
import { createMedia } from '@mostajs/media-server'
|
|
314
|
+
|
|
315
|
+
const memRepo = createInMemoryRepo() // implémentation locale du contract
|
|
316
|
+
const memStorage = createInMemoryStorage()
|
|
317
|
+
|
|
318
|
+
const svc = createMedia({ storage: memStorage, repo: memRepo })
|
|
319
|
+
const { row, signedUrl } = await svc.createRecording({
|
|
320
|
+
accountId: 'acc-1',
|
|
321
|
+
body: new Uint8Array([1, 2, 3]),
|
|
322
|
+
mimeType: 'video/webm',
|
|
323
|
+
durationMs: 5000,
|
|
324
|
+
})
|
|
325
|
+
console.assert(row.status === 'ready')
|
|
326
|
+
console.assert(signedUrl.startsWith('http'))
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Roadmap
|
|
332
|
+
|
|
333
|
+
| Version | Périmètre | ETA |
|
|
334
|
+
|---------|-----------|-----|
|
|
335
|
+
| 0.1.0 | Scaffold + factory + contract | ✅ Livré 19/05/2026 |
|
|
336
|
+
| 0.2.0 | Routes Next.js App Router prêtes (`/handlers/next.ts`) + smoke camera-studio | T+3 j |
|
|
337
|
+
| 0.3.0 | Tests Vitest (mock storage + repo) | T+7 j |
|
|
338
|
+
| 0.4.0 | Webhook events lifecycle (uploaded, ready, deleted) via `@mostajs/notifications` adapter | T+10 j |
|
|
339
|
+
| 0.5.0 | Quota par tenant + GC blobs orphelins | T+14 j |
|
|
340
|
+
| 1.0.0 | Stable API, smoke iquesta + camera-studio + 1 consumer externe | T+21 j |
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Liens
|
|
345
|
+
|
|
346
|
+
- Design canonique : [`mostajs/mosta-media/docs/MULTI-SOURCE-AND-RESUME-DESIGN.md`](../mosta-media/docs/MULTI-SOURCE-AND-RESUME-DESIGN.md) §3 P1
|
|
347
|
+
- Plan iquesta : [`SolutionCh/iquesta/docs/MEDIA-INTEGRATION-COURSE-BUILDER.md`](../../SolutionCh/iquesta/docs/MEDIA-INTEGRATION-COURSE-BUILDER.md)
|
|
348
|
+
- Sibling client : [`@mostajs/media`](../mosta-media)
|
|
349
|
+
- Storage driver : [`@mostajs/storage`](https://github.com/apolocine/mosta-storage)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { MediaRepository } from './schema.js';
|
|
2
|
+
import type { MediaService } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Surface minimale attendue du storage côté factory. Compatible avec
|
|
5
|
+
* `@mostajs/storage` FileStore mais déclarée localement pour ne pas créer de
|
|
6
|
+
* hard-dep build-time (peer dep seulement).
|
|
7
|
+
*/
|
|
8
|
+
export interface StorageLike {
|
|
9
|
+
/** Upload binaire. Retourne ref + size + checksum. */
|
|
10
|
+
put(args: {
|
|
11
|
+
bucket: string;
|
|
12
|
+
path: string;
|
|
13
|
+
body: Uint8Array | Blob | ArrayBuffer;
|
|
14
|
+
mimeType: string;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
size: number;
|
|
17
|
+
checksum?: string | null;
|
|
18
|
+
}>;
|
|
19
|
+
/** Hard-delete d'un objet. */
|
|
20
|
+
delete(args: {
|
|
21
|
+
bucket: string;
|
|
22
|
+
path: string;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
/** Signed URL pour playback browser-direct. */
|
|
25
|
+
signedUrl(args: {
|
|
26
|
+
bucket: string;
|
|
27
|
+
path: string;
|
|
28
|
+
ttlSec?: number;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
url: string;
|
|
31
|
+
expiresAt: number;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
/** Options du factory. */
|
|
35
|
+
export interface CreateMediaOptions {
|
|
36
|
+
/** Adapter storage (FileStore @mostajs/storage ou shim équivalent). */
|
|
37
|
+
storage: StorageLike;
|
|
38
|
+
/** Repository métadonnée ORM (à fournir par le consumer). */
|
|
39
|
+
repo: MediaRepository;
|
|
40
|
+
/** Bucket par défaut (peut être overridé per-request via input.bucket). */
|
|
41
|
+
defaultBucket?: string;
|
|
42
|
+
/** TTL des signed URLs en secondes (défaut 1h). */
|
|
43
|
+
signedUrlTtlSec?: number;
|
|
44
|
+
/** Générateur d'id custom (défaut ULID-like). */
|
|
45
|
+
generateId?: () => string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Construit un MediaService prêt à servir des routes HTTP. Le handler reste
|
|
49
|
+
* responsable du RBAC (accountId depuis la session) et de la sérialisation
|
|
50
|
+
* HTTP — le factory expose un service pur.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createMedia(opts: CreateMediaOptions): MediaService;
|
|
53
|
+
//# sourceMappingURL=factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,eAAe,EAChB,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EACV,YAAY,EAEb,MAAM,YAAY,CAAA;AAEnB;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,sDAAsD;IACtD,GAAG,CAAC,IAAI,EAAE;QACR,MAAM,EAAE,MAAM,CAAA;QACd,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,WAAW,CAAA;QACrC,QAAQ,EAAE,MAAM,CAAA;KACjB,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAA;IACvD,8BAA8B;IAC9B,MAAM,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7D,+CAA+C;IAC/C,SAAS,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAChH;AAED,0BAA0B;AAC1B,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,OAAO,EAAE,WAAW,CAAA;IACpB,6DAA6D;IAC7D,IAAI,EAAE,eAAe,CAAA;IACrB,2EAA2E;IAC3E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mDAAmD;IACnD,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,MAAM,CAAA;CAC1B;AA2BD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,YAAY,CAsIlE"}
|
package/dist/factory.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// @mostajs/media-server — createMedia factory
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
//
|
|
4
|
+
// Factory qui pluge un repository ORM (MediaRepository) sur un driver storage
|
|
5
|
+
// (@mostajs/storage FileStore) pour produire un MediaService. Le route handler
|
|
6
|
+
// (Next.js App Router, Express, …) consomme la surface MediaService sans rien
|
|
7
|
+
// connaître du storage backend ni du dialecte ORM utilisé.
|
|
8
|
+
const DEFAULT_BUCKET = 'media-recordings';
|
|
9
|
+
const DEFAULT_TTL = 60 * 60;
|
|
10
|
+
function defaultId() {
|
|
11
|
+
// Format compact base36 ~ ULID-friendly. Pas de hard-dep ulid pour scaffold v0.1.0.
|
|
12
|
+
return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
|
|
13
|
+
}
|
|
14
|
+
function deriveKind(mimeType) {
|
|
15
|
+
if (mimeType.startsWith('video/'))
|
|
16
|
+
return 'video';
|
|
17
|
+
if (mimeType.startsWith('audio/'))
|
|
18
|
+
return 'audio';
|
|
19
|
+
if (mimeType.startsWith('image/'))
|
|
20
|
+
return 'image';
|
|
21
|
+
return 'data'; // application/json, application/octet-stream, etc.
|
|
22
|
+
}
|
|
23
|
+
function extFromMime(mimeType) {
|
|
24
|
+
if (mimeType.includes('webm'))
|
|
25
|
+
return 'webm';
|
|
26
|
+
if (mimeType.includes('mp4'))
|
|
27
|
+
return 'mp4';
|
|
28
|
+
if (mimeType.includes('ogg'))
|
|
29
|
+
return 'ogg';
|
|
30
|
+
if (mimeType.includes('png'))
|
|
31
|
+
return 'png';
|
|
32
|
+
if (mimeType.includes('jpeg') || mimeType.includes('jpg'))
|
|
33
|
+
return 'jpg';
|
|
34
|
+
if (mimeType.includes('json'))
|
|
35
|
+
return 'json';
|
|
36
|
+
return 'bin';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Construit un MediaService prêt à servir des routes HTTP. Le handler reste
|
|
40
|
+
* responsable du RBAC (accountId depuis la session) et de la sérialisation
|
|
41
|
+
* HTTP — le factory expose un service pur.
|
|
42
|
+
*/
|
|
43
|
+
export function createMedia(opts) {
|
|
44
|
+
const { storage, repo } = opts;
|
|
45
|
+
const defaultBucket = opts.defaultBucket ?? DEFAULT_BUCKET;
|
|
46
|
+
const ttl = opts.signedUrlTtlSec ?? DEFAULT_TTL;
|
|
47
|
+
const newId = opts.generateId ?? defaultId;
|
|
48
|
+
async function buildSignedUrl(row) {
|
|
49
|
+
const { url, expiresAt } = await storage.signedUrl({
|
|
50
|
+
bucket: row.bucket,
|
|
51
|
+
path: row.key,
|
|
52
|
+
ttlSec: ttl,
|
|
53
|
+
});
|
|
54
|
+
return { row, signedUrl: url, signedUrlExpiresAt: expiresAt };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
async createRecording(input) {
|
|
58
|
+
if (!input.accountId)
|
|
59
|
+
throw new Error('createRecording: accountId is required');
|
|
60
|
+
if (!input.mimeType)
|
|
61
|
+
throw new Error('createRecording: mimeType is required');
|
|
62
|
+
const id = newId();
|
|
63
|
+
const bucket = input.bucket ?? defaultBucket;
|
|
64
|
+
const kind = input.kind ?? deriveKind(input.mimeType);
|
|
65
|
+
const key = `${input.accountId}/${id}.${extFromMime(input.mimeType)}`;
|
|
66
|
+
// 1. Insert row status=uploading. Permet de tracer un upload qui foire.
|
|
67
|
+
let row = await repo.insert({
|
|
68
|
+
accountId: input.accountId,
|
|
69
|
+
ownerEmail: input.ownerEmail ?? null,
|
|
70
|
+
bucket,
|
|
71
|
+
key,
|
|
72
|
+
mimeType: input.mimeType,
|
|
73
|
+
sizeBytes: 0,
|
|
74
|
+
durationMs: input.durationMs,
|
|
75
|
+
kind,
|
|
76
|
+
status: 'uploading',
|
|
77
|
+
sessionId: input.sessionId ?? null,
|
|
78
|
+
takeIndex: input.takeIndex ?? null,
|
|
79
|
+
metadata: input.metadata ?? null,
|
|
80
|
+
id,
|
|
81
|
+
});
|
|
82
|
+
// 2. Put storage. Failure → row.status=failed (auditable).
|
|
83
|
+
let putResult;
|
|
84
|
+
try {
|
|
85
|
+
putResult = await storage.put({
|
|
86
|
+
bucket, path: key, body: input.body, mimeType: input.mimeType,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
await repo.update(row.id, { status: 'failed', updatedAt: new Date() });
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
// 3. Update row status=ready avec taille + checksum réels.
|
|
94
|
+
row = await repo.update(row.id, {
|
|
95
|
+
status: 'ready',
|
|
96
|
+
sizeBytes: putResult.size,
|
|
97
|
+
checksum: putResult.checksum ?? null,
|
|
98
|
+
updatedAt: new Date(),
|
|
99
|
+
});
|
|
100
|
+
const signed = await buildSignedUrl(row);
|
|
101
|
+
return {
|
|
102
|
+
row: signed.row,
|
|
103
|
+
signedUrl: signed.signedUrl,
|
|
104
|
+
signedUrlExpiresAt: signed.signedUrlExpiresAt,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
async updateRecording(id, input) {
|
|
108
|
+
const existing = await repo.findById(id);
|
|
109
|
+
if (!existing)
|
|
110
|
+
return null;
|
|
111
|
+
// Ré-uploade le blob sur la MÊME key (écrase l'objet storage).
|
|
112
|
+
const mimeType = input.mimeType ?? existing.mimeType;
|
|
113
|
+
let putResult;
|
|
114
|
+
try {
|
|
115
|
+
putResult = await storage.put({
|
|
116
|
+
bucket: existing.bucket, path: existing.key, body: input.body, mimeType,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
await repo.update(id, { status: 'failed', updatedAt: new Date() });
|
|
121
|
+
throw e;
|
|
122
|
+
}
|
|
123
|
+
const row = await repo.update(id, {
|
|
124
|
+
status: 'ready',
|
|
125
|
+
mimeType,
|
|
126
|
+
sizeBytes: putResult.size,
|
|
127
|
+
checksum: putResult.checksum ?? null,
|
|
128
|
+
durationMs: input.durationMs ?? existing.durationMs,
|
|
129
|
+
metadata: input.metadata ?? existing.metadata,
|
|
130
|
+
updatedAt: new Date(),
|
|
131
|
+
});
|
|
132
|
+
const signed = await buildSignedUrl(row);
|
|
133
|
+
return {
|
|
134
|
+
row: signed.row,
|
|
135
|
+
signedUrl: signed.signedUrl,
|
|
136
|
+
signedUrlExpiresAt: signed.signedUrlExpiresAt,
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
async getMedia(id) {
|
|
140
|
+
const row = await repo.findById(id);
|
|
141
|
+
if (!row || row.status !== 'ready')
|
|
142
|
+
return null;
|
|
143
|
+
return buildSignedUrl(row);
|
|
144
|
+
},
|
|
145
|
+
async reissueSignedUrl(id, ttlSec) {
|
|
146
|
+
const row = await repo.findById(id);
|
|
147
|
+
if (!row || row.status !== 'ready')
|
|
148
|
+
return null;
|
|
149
|
+
const { url, expiresAt } = await storage.signedUrl({
|
|
150
|
+
bucket: row.bucket, path: row.key, ttlSec: ttlSec ?? ttl,
|
|
151
|
+
});
|
|
152
|
+
return { row, signedUrl: url, signedUrlExpiresAt: expiresAt };
|
|
153
|
+
},
|
|
154
|
+
async deleteMedia(id) {
|
|
155
|
+
const row = await repo.findById(id);
|
|
156
|
+
if (!row)
|
|
157
|
+
return;
|
|
158
|
+
// Soft-delete row d'abord (idempotent), purge storage ensuite.
|
|
159
|
+
await repo.update(id, { status: 'deleted', updatedAt: new Date() });
|
|
160
|
+
try {
|
|
161
|
+
await storage.delete({ bucket: row.bucket, path: row.key });
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
// Si la purge échoue, on log mais on garde la row soft-deleted.
|
|
165
|
+
// Le consumer peut prévoir un GC périodique pour les blobs orphelins.
|
|
166
|
+
// eslint-disable-next-line no-console
|
|
167
|
+
console.warn('[mosta-media-server] storage.delete failed for', row.bucket, row.key, e);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
async listForAccount(accountId, filter) {
|
|
171
|
+
return repo.list(accountId, filter);
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { MediaService } from '../types.js';
|
|
2
|
+
/** Contract pour extraire l'accountId tenant depuis la requête HTTP.
|
|
3
|
+
* L'implémentation typique : lire la session JWT/cookie, charger l'account
|
|
4
|
+
* associé. Retourner null = 401. Voir mémoire `feedback_account_id_vs_user_id`. */
|
|
5
|
+
export type GetAccountIdFromRequest = (req: Request) => Promise<string | null> | string | null;
|
|
6
|
+
/** Validation MIME — défaut accepte video/* | audio/* | image/*. */
|
|
7
|
+
export type MimeAcceptor = (mimeType: string) => boolean;
|
|
8
|
+
export interface RecordingsRouteOptions {
|
|
9
|
+
/** Service factory issu de `createMedia({ storage, repo })`. */
|
|
10
|
+
mediaService: MediaService;
|
|
11
|
+
/** Extraction tenant + auth. Retourne null pour 401. */
|
|
12
|
+
getAccountIdFromRequest: GetAccountIdFromRequest;
|
|
13
|
+
/** Optionnel : restreindre MIME (default video/audio/image). */
|
|
14
|
+
acceptMimeType?: MimeAcceptor;
|
|
15
|
+
/** Cap hard taille blob (default 500 MB). */
|
|
16
|
+
maxSizeBytes?: number;
|
|
17
|
+
/** Optionnel : extraire ownerEmail si l'auth le fournit (audit row). */
|
|
18
|
+
getOwnerEmail?: (req: Request) => Promise<string | null> | string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface RecordingsRouteHandlers {
|
|
21
|
+
/** POST /api/media/recordings — multipart/form-data { blob, mimeType, durationMs, sessionId?, takeIndex?, metadata? }. */
|
|
22
|
+
POST(req: Request): Promise<Response>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Factory du handler POST /api/media/recordings.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* // app/api/media/recordings/route.ts
|
|
30
|
+
* import { createRecordingsRoute } from '@mostajs/media-server'
|
|
31
|
+
* import { mediaService } from '@/lib/media-service'
|
|
32
|
+
* import { getAccountIdFromSession } from '@/lib/session'
|
|
33
|
+
*
|
|
34
|
+
* export const runtime = 'nodejs'
|
|
35
|
+
* const { POST } = createRecordingsRoute({
|
|
36
|
+
* mediaService,
|
|
37
|
+
* getAccountIdFromRequest: (req) => getAccountIdFromSession(req),
|
|
38
|
+
* maxSizeBytes: 200 * 1024 * 1024,
|
|
39
|
+
* })
|
|
40
|
+
* export { POST }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function createRecordingsRoute(opts: RecordingsRouteOptions): RecordingsRouteHandlers;
|
|
44
|
+
export interface MediaByIdRouteOptions {
|
|
45
|
+
mediaService: MediaService;
|
|
46
|
+
getAccountIdFromRequest: GetAccountIdFromRequest;
|
|
47
|
+
}
|
|
48
|
+
/** Next.js App Router context typing. Compatible Next ≥ 15 (params: Promise) ET Next ≤ 14 (params: object). */
|
|
49
|
+
type RouteParamsCtx = {
|
|
50
|
+
params: {
|
|
51
|
+
id: string;
|
|
52
|
+
} | Promise<{
|
|
53
|
+
id: string;
|
|
54
|
+
}>;
|
|
55
|
+
};
|
|
56
|
+
export interface MediaByIdRouteHandlers {
|
|
57
|
+
/** GET /api/media/[id] → row + signed URL frais. 404 si absent, 403 si tenant mismatch. */
|
|
58
|
+
GET(req: Request, ctx: RouteParamsCtx): Promise<Response>;
|
|
59
|
+
/** PUT /api/media/[id] → remplace le blob d'une row existante (saveOrUpdate).
|
|
60
|
+
* multipart/form-data { blob, mimeType?, durationMs?, metadata? }. */
|
|
61
|
+
PUT(req: Request, ctx: RouteParamsCtx): Promise<Response>;
|
|
62
|
+
/** DELETE /api/media/[id] → soft-delete row + purge storage. */
|
|
63
|
+
DELETE(req: Request, ctx: RouteParamsCtx): Promise<Response>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Factory pour GET + PUT + DELETE /api/media/[id]. Le consumer ré-exporte
|
|
67
|
+
* `{ GET, PUT, DELETE }` depuis `app/api/media/[id]/route.ts`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function createMediaByIdRoute(opts: MediaByIdRouteOptions): MediaByIdRouteHandlers;
|
|
70
|
+
export {};
|
|
71
|
+
//# sourceMappingURL=next.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../../src/handlers/next.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C;;oFAEoF;AACpF,MAAM,MAAM,uBAAuB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,CAAA;AAE9F,oEAAoE;AACpE,MAAM,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAA;AAExD,MAAM,WAAW,sBAAsB;IACrC,gEAAgE;IAChE,YAAY,EAAE,YAAY,CAAA;IAC1B,wDAAwD;IACxD,uBAAuB,EAAE,uBAAuB,CAAA;IAChD,gEAAgE;IAChE,cAAc,CAAC,EAAE,YAAY,CAAA;IAC7B,6CAA6C;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,CAAA;CACzE;AAED,MAAM,WAAW,uBAAuB;IACtC,0HAA0H;IAC1H,IAAI,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CACtC;AAMD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,sBAAsB,GAAG,uBAAuB,CAiF3F;AAID,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,YAAY,CAAA;IAC1B,uBAAuB,EAAE,uBAAuB,CAAA;CACjD;AAED,+GAA+G;AAC/G,KAAK,cAAc,GAAG;IACpB,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACjD,CAAA;AAED,MAAM,WAAW,sBAAsB;IACrC,2FAA2F;IAC3F,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACzD;2EACuE;IACvE,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACzD,gEAAgE;IAChE,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC7D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,qBAAqB,GAAG,sBAAsB,CAgFxF"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// @mostajs/media-server — Next.js App Router handler factories
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
//
|
|
4
|
+
// Factories qui transforment un MediaService + une politique RBAC en handlers
|
|
5
|
+
// HTTP prêts à l'emploi pour Next.js App Router. Le consumer instancie une
|
|
6
|
+
// fois côté serveur (ex: lib/media-service.ts), branche les handlers dans
|
|
7
|
+
// app/api/media/recordings/route.ts + app/api/media/[id]/route.ts.
|
|
8
|
+
const DEFAULT_MAX_SIZE = 500 * 1024 * 1024; // 500 MB
|
|
9
|
+
const defaultMimeAccept = (mime) => mime.startsWith('video/') || mime.startsWith('audio/') || mime.startsWith('image/');
|
|
10
|
+
/**
|
|
11
|
+
* Factory du handler POST /api/media/recordings.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* // app/api/media/recordings/route.ts
|
|
16
|
+
* import { createRecordingsRoute } from '@mostajs/media-server'
|
|
17
|
+
* import { mediaService } from '@/lib/media-service'
|
|
18
|
+
* import { getAccountIdFromSession } from '@/lib/session'
|
|
19
|
+
*
|
|
20
|
+
* export const runtime = 'nodejs'
|
|
21
|
+
* const { POST } = createRecordingsRoute({
|
|
22
|
+
* mediaService,
|
|
23
|
+
* getAccountIdFromRequest: (req) => getAccountIdFromSession(req),
|
|
24
|
+
* maxSizeBytes: 200 * 1024 * 1024,
|
|
25
|
+
* })
|
|
26
|
+
* export { POST }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function createRecordingsRoute(opts) {
|
|
30
|
+
const maxSize = opts.maxSizeBytes ?? DEFAULT_MAX_SIZE;
|
|
31
|
+
const acceptMime = opts.acceptMimeType ?? defaultMimeAccept;
|
|
32
|
+
return {
|
|
33
|
+
async POST(req) {
|
|
34
|
+
const accountId = await opts.getAccountIdFromRequest(req);
|
|
35
|
+
if (!accountId)
|
|
36
|
+
return jsonResponse({ error: 'unauthorized' }, 401);
|
|
37
|
+
let form;
|
|
38
|
+
try {
|
|
39
|
+
form = await req.formData();
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
return jsonResponse({ error: 'invalid_multipart', detail: e.message }, 400);
|
|
43
|
+
}
|
|
44
|
+
const blob = form.get('blob');
|
|
45
|
+
if (!(blob instanceof Blob)) {
|
|
46
|
+
return jsonResponse({ error: 'blob_required' }, 400);
|
|
47
|
+
}
|
|
48
|
+
const mimeType = form.get('mimeType') || blob.type || 'application/octet-stream';
|
|
49
|
+
if (!acceptMime(mimeType)) {
|
|
50
|
+
return jsonResponse({ error: 'unsupported_mime', mimeType }, 415);
|
|
51
|
+
}
|
|
52
|
+
if (blob.size > maxSize) {
|
|
53
|
+
return jsonResponse({ error: 'too_large', size: blob.size, maxSize }, 413);
|
|
54
|
+
}
|
|
55
|
+
if (blob.size === 0) {
|
|
56
|
+
return jsonResponse({ error: 'empty_blob' }, 400);
|
|
57
|
+
}
|
|
58
|
+
const durationMs = Number(form.get('durationMs') ?? 0);
|
|
59
|
+
const sessionId = form.get('sessionId') || undefined;
|
|
60
|
+
const takeIndexRaw = form.get('takeIndex');
|
|
61
|
+
const takeIndex = takeIndexRaw != null ? Number(takeIndexRaw) : undefined;
|
|
62
|
+
const metadataRaw = form.get('metadata');
|
|
63
|
+
let metadata;
|
|
64
|
+
if (typeof metadataRaw === 'string' && metadataRaw.length > 0) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(metadataRaw);
|
|
67
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
68
|
+
metadata = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch { /* metadata invalide → ignoré, pas bloquant */ }
|
|
72
|
+
}
|
|
73
|
+
const ownerEmail = opts.getOwnerEmail
|
|
74
|
+
? (await opts.getOwnerEmail(req)) ?? undefined
|
|
75
|
+
: undefined;
|
|
76
|
+
try {
|
|
77
|
+
const body = new Uint8Array(await blob.arrayBuffer());
|
|
78
|
+
const result = await opts.mediaService.createRecording({
|
|
79
|
+
accountId,
|
|
80
|
+
ownerEmail,
|
|
81
|
+
body,
|
|
82
|
+
mimeType,
|
|
83
|
+
durationMs: Number.isFinite(durationMs) ? durationMs : 0,
|
|
84
|
+
sessionId,
|
|
85
|
+
takeIndex,
|
|
86
|
+
metadata,
|
|
87
|
+
});
|
|
88
|
+
return jsonResponse(result, 201);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
return jsonResponse({ error: 'create_failed', detail: e.message }, 500);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Factory pour GET + PUT + DELETE /api/media/[id]. Le consumer ré-exporte
|
|
98
|
+
* `{ GET, PUT, DELETE }` depuis `app/api/media/[id]/route.ts`.
|
|
99
|
+
*/
|
|
100
|
+
export function createMediaByIdRoute(opts) {
|
|
101
|
+
async function resolveParams(ctx) {
|
|
102
|
+
return ctx.params instanceof Promise ? await ctx.params : ctx.params;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
async GET(req, ctx) {
|
|
106
|
+
const accountId = await opts.getAccountIdFromRequest(req);
|
|
107
|
+
if (!accountId)
|
|
108
|
+
return jsonResponse({ error: 'unauthorized' }, 401);
|
|
109
|
+
const { id } = await resolveParams(ctx);
|
|
110
|
+
const result = await opts.mediaService.getMedia(id);
|
|
111
|
+
if (!result)
|
|
112
|
+
return jsonResponse({ error: 'not_found' }, 404);
|
|
113
|
+
if (result.row.accountId !== accountId) {
|
|
114
|
+
return jsonResponse({ error: 'forbidden' }, 403);
|
|
115
|
+
}
|
|
116
|
+
return jsonResponse(result);
|
|
117
|
+
},
|
|
118
|
+
async PUT(req, ctx) {
|
|
119
|
+
const accountId = await opts.getAccountIdFromRequest(req);
|
|
120
|
+
if (!accountId)
|
|
121
|
+
return jsonResponse({ error: 'unauthorized' }, 401);
|
|
122
|
+
const { id } = await resolveParams(ctx);
|
|
123
|
+
// RBAC : la row doit appartenir au tenant de la session.
|
|
124
|
+
const existing = await opts.mediaService.getMedia(id);
|
|
125
|
+
if (!existing)
|
|
126
|
+
return jsonResponse({ error: 'not_found' }, 404);
|
|
127
|
+
if (existing.row.accountId !== accountId) {
|
|
128
|
+
return jsonResponse({ error: 'forbidden' }, 403);
|
|
129
|
+
}
|
|
130
|
+
let form;
|
|
131
|
+
try {
|
|
132
|
+
form = await req.formData();
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
return jsonResponse({ error: 'invalid_multipart', detail: e.message }, 400);
|
|
136
|
+
}
|
|
137
|
+
const blob = form.get('blob');
|
|
138
|
+
if (!(blob instanceof Blob) || blob.size === 0) {
|
|
139
|
+
return jsonResponse({ error: 'blob_required' }, 400);
|
|
140
|
+
}
|
|
141
|
+
const mimeType = form.get('mimeType') || undefined;
|
|
142
|
+
const durationMsRaw = form.get('durationMs');
|
|
143
|
+
const durationMs = durationMsRaw != null ? Number(durationMsRaw) : undefined;
|
|
144
|
+
const metadataRaw = form.get('metadata');
|
|
145
|
+
let metadata;
|
|
146
|
+
if (typeof metadataRaw === 'string' && metadataRaw.length > 0) {
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(metadataRaw);
|
|
149
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
150
|
+
metadata = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch { /* metadata invalide → ignoré */ }
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const body = new Uint8Array(await blob.arrayBuffer());
|
|
157
|
+
const result = await opts.mediaService.updateRecording(id, {
|
|
158
|
+
body, mimeType, durationMs, metadata,
|
|
159
|
+
});
|
|
160
|
+
if (!result)
|
|
161
|
+
return jsonResponse({ error: 'not_found' }, 404);
|
|
162
|
+
return jsonResponse(result, 200);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
return jsonResponse({ error: 'update_failed', detail: e.message }, 500);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
async DELETE(req, ctx) {
|
|
169
|
+
const accountId = await opts.getAccountIdFromRequest(req);
|
|
170
|
+
if (!accountId)
|
|
171
|
+
return jsonResponse({ error: 'unauthorized' }, 401);
|
|
172
|
+
const { id } = await resolveParams(ctx);
|
|
173
|
+
const existing = await opts.mediaService.getMedia(id);
|
|
174
|
+
if (existing && existing.row.accountId !== accountId) {
|
|
175
|
+
return jsonResponse({ error: 'forbidden' }, 403);
|
|
176
|
+
}
|
|
177
|
+
await opts.mediaService.deleteMedia(id);
|
|
178
|
+
return jsonResponse({ ok: true });
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// ─── Helpers ───────────────────────────────────────────────────────────
|
|
183
|
+
function jsonResponse(body, status = 200) {
|
|
184
|
+
return new Response(JSON.stringify(body), {
|
|
185
|
+
status,
|
|
186
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=next.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next.js","sourceRoot":"","sources":["../../src/handlers/next.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { MediaKind, MediaStatus, MediaRow, MediaListFilter, MediaListResult, MediaRepository, } from './schema.js';
|
|
2
|
+
export type { CreateRecordingInput, CreateRecordingResult, MediaWithSignedUrl, MediaService, } from './types.js';
|
|
3
|
+
export type { StorageLike, CreateMediaOptions } from './factory.js';
|
|
4
|
+
export { createMedia } from './factory.js';
|
|
5
|
+
export type { RecordingsRouteOptions, RecordingsRouteHandlers, MediaByIdRouteOptions, MediaByIdRouteHandlers, GetAccountIdFromRequest, MimeAcceptor, } from './handlers/next.js';
|
|
6
|
+
export { createRecordingsRoute, createMediaByIdRoute } from './handlers/next.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,YAAY,EACV,SAAS,EAAE,WAAW,EAAE,QAAQ,EAChC,eAAe,EAAE,eAAe,EAChC,eAAe,GAChB,MAAM,aAAa,CAAA;AAEpB,YAAY,EACV,oBAAoB,EAAE,qBAAqB,EAC3C,kBAAkB,EAAE,YAAY,GACjC,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACnE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAG1C,YAAY,EACV,sBAAsB,EAAE,uBAAuB,EAC/C,qBAAqB,EAAE,sBAAsB,EAC7C,uBAAuB,EAAE,YAAY,GACtC,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,0CAA0C;AAc1C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAQ1C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA"}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Catégorie sémantique de la prise. `data` = blob non-média (JSON ProjectFile,
|
|
2
|
+
* manifests, etc.) qui passe par la même pipeline storage/repo mais ne doit
|
|
3
|
+
* pas être routé vers un player. */
|
|
4
|
+
export type MediaKind = 'video' | 'audio' | 'image' | 'data';
|
|
5
|
+
/** Cycle de vie d'une row Media. */
|
|
6
|
+
export type MediaStatus =
|
|
7
|
+
/** Row créée, blob pas encore uploadé (réservé pour le upload-then-finalize pattern). */
|
|
8
|
+
'uploading'
|
|
9
|
+
/** Blob présent en storage, signed URL exploitable. */
|
|
10
|
+
| 'ready'
|
|
11
|
+
/** Soft-delete : row marquée mais blob purgé du storage. */
|
|
12
|
+
| 'deleted'
|
|
13
|
+
/** Erreur upload/finalize — row conservée pour debug, blob inexistant ou orphelin. */
|
|
14
|
+
| 'failed';
|
|
15
|
+
/** Row métadonnée persistée. Les bytes vivent dans le storage (FS/S3/…). */
|
|
16
|
+
export interface MediaRow {
|
|
17
|
+
/** Identifiant interne — UUID ou ULID. Stable cross-restart. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Tenant propriétaire — clé d'isolation multi-tenant. */
|
|
20
|
+
accountId: string;
|
|
21
|
+
/** Email du créateur (audit). Optionnel selon le repo. */
|
|
22
|
+
ownerEmail?: string | null;
|
|
23
|
+
/** Nom logique du bucket dans @mostajs/storage. Par défaut `media-recordings`. */
|
|
24
|
+
bucket: string;
|
|
25
|
+
/** Path relatif dans le bucket. Convention `<accountId>/<id>.<ext>`. */
|
|
26
|
+
key: string;
|
|
27
|
+
/** Type MIME — validé côté factory au moment du createRecording. */
|
|
28
|
+
mimeType: string;
|
|
29
|
+
/** Taille en octets. */
|
|
30
|
+
sizeBytes: number;
|
|
31
|
+
/** Durée en ms — client-mesuré pour audio/video, 0 pour image. */
|
|
32
|
+
durationMs: number;
|
|
33
|
+
/** Kind sémantique — utile au consumer pour router vers le bon player. */
|
|
34
|
+
kind: MediaKind;
|
|
35
|
+
/** Cycle de vie. */
|
|
36
|
+
status: MediaStatus;
|
|
37
|
+
/** Identifiant de session multi-take (optionnel — permet de grouper des takes). */
|
|
38
|
+
sessionId?: string | null;
|
|
39
|
+
/** Index dans la session multi-take. Si pas de session, 0. */
|
|
40
|
+
takeIndex?: number | null;
|
|
41
|
+
/** Hash sha256 hex — fourni par @mostajs/storage à l'upload. */
|
|
42
|
+
checksum?: string | null;
|
|
43
|
+
/** Métadonnées libres (subtitle path, thumbnail key, etc.). */
|
|
44
|
+
metadata?: Record<string, string> | null;
|
|
45
|
+
/** Timestamp création. */
|
|
46
|
+
createdAt: Date | string;
|
|
47
|
+
/** Timestamp dernière mise à jour. */
|
|
48
|
+
updatedAt?: Date | string | null;
|
|
49
|
+
}
|
|
50
|
+
/** Filtre paginé pour `listForAccount`. */
|
|
51
|
+
export interface MediaListFilter {
|
|
52
|
+
/** Filtre par session (récupérer les takes d'une session multi-take). */
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
/** Filtre par status (par défaut `ready` seulement). */
|
|
55
|
+
status?: MediaStatus | MediaStatus[];
|
|
56
|
+
/** Filtre par kind. */
|
|
57
|
+
kind?: MediaKind | MediaKind[];
|
|
58
|
+
/** Tri (champ + direction). Défaut createdAt desc. */
|
|
59
|
+
orderBy?: {
|
|
60
|
+
field: 'createdAt' | 'updatedAt' | 'sizeBytes';
|
|
61
|
+
direction: 'asc' | 'desc';
|
|
62
|
+
};
|
|
63
|
+
/** Limite (pagination forward-only). Défaut 50, max 200. */
|
|
64
|
+
limit?: number;
|
|
65
|
+
/** Curseur opaque pour la page suivante (id de la dernière row). */
|
|
66
|
+
cursor?: string;
|
|
67
|
+
}
|
|
68
|
+
/** Page résultat de `listForAccount`. */
|
|
69
|
+
export interface MediaListResult {
|
|
70
|
+
items: MediaRow[];
|
|
71
|
+
/** Curseur pour la page suivante. Absent si plus de pages. */
|
|
72
|
+
nextCursor?: string;
|
|
73
|
+
/** Total approximatif (count exact optionnel — coûteux côté DB). */
|
|
74
|
+
total?: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Contract du repository Media — implémentation à fournir par le consumer
|
|
78
|
+
* (typiquement au-dessus de @mostajs/orm, Prisma, ou un repo maison).
|
|
79
|
+
*
|
|
80
|
+
* Le repo manipule UNIQUEMENT la row métadonnée. Les bytes sont gérés par
|
|
81
|
+
* @mostajs/storage (paramètre `storage` du factory). Cette séparation permet
|
|
82
|
+
* de tester le factory en mockant les 2 surfaces indépendamment.
|
|
83
|
+
*/
|
|
84
|
+
export interface MediaRepository {
|
|
85
|
+
/** Insère une nouvelle row. Doit générer `id` si absent dans `data`. */
|
|
86
|
+
insert(data: Omit<MediaRow, 'id' | 'createdAt'> & {
|
|
87
|
+
id?: string;
|
|
88
|
+
}): Promise<MediaRow>;
|
|
89
|
+
/** Récupère une row par id (peu importe le tenant — RBAC géré côté handler). */
|
|
90
|
+
findById(id: string): Promise<MediaRow | null>;
|
|
91
|
+
/** Update partiel — typiquement status + updatedAt. */
|
|
92
|
+
update(id: string, patch: Partial<Omit<MediaRow, 'id' | 'createdAt'>>): Promise<MediaRow>;
|
|
93
|
+
/** Hard-delete row (le storage est purgé séparément par le factory). */
|
|
94
|
+
delete(id: string): Promise<void>;
|
|
95
|
+
/** Liste paginée pour un tenant. */
|
|
96
|
+
list(accountId: string, filter?: MediaListFilter): Promise<MediaListResult>;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAQA;;qCAEqC;AACrC,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;AAE5D,oCAAoC;AACpC,MAAM,MAAM,WAAW;AACrB,yFAAyF;AACvF,WAAW;AACb,uDAAuD;GACrD,OAAO;AACT,4DAA4D;GAC1D,SAAS;AACX,sFAAsF;GACpF,QAAQ,CAAA;AAEZ,4EAA4E;AAC5E,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,EAAE,EAAE,MAAM,CAAA;IACV,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAA;IACjB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAA;IACX,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAA;IAChB,wBAAwB;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,IAAI,EAAE,SAAS,CAAA;IACf,oBAAoB;IACpB,MAAM,EAAE,WAAW,CAAA;IACnB,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;IACxC,0BAA0B;IAC1B,SAAS,EAAE,IAAI,GAAG,MAAM,CAAA;IACxB,sCAAsC;IACtC,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;CACjC;AAED,2CAA2C;AAC3C,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,MAAM,CAAC,EAAE,WAAW,GAAG,WAAW,EAAE,CAAA;IACpC,uBAAuB;IACvB,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,EAAE,CAAA;IAC9B,sDAAsD;IACtD,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;QAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAA;IACvF,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,yCAAyC;AACzC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,GAAG,WAAW,CAAC,GAAG;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACrF,gFAAgF;IAChF,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IAC9C,uDAAuD;IACvD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,GAAG,WAAW,CAAC,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACzF,wEAAwE;IACxE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,oCAAoC;IACpC,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;CAC5E"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @mostajs/media-server — Domain schema
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
//
|
|
4
|
+
// Contract pour la row "Media" persistée en DB. Le consumer fournit le repo
|
|
5
|
+
// qui implémente cette forme. Volontairement minimal : pas de relations ORM
|
|
6
|
+
// fortes, pas de hooks lifecycle — laissés au consumer (qui peut câbler son
|
|
7
|
+
// ORM préféré via @mostajs/orm ou tout autre data layer).
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,0CAA0C;AAC1C,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,4EAA4E;AAC5E,0DAA0D"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { MediaKind, MediaRow, MediaListFilter, MediaListResult } from './schema.js';
|
|
2
|
+
/** Input du POST recording — multipart/form-data côté HTTP, objet typé côté factory. */
|
|
3
|
+
export interface CreateRecordingInput {
|
|
4
|
+
accountId: string;
|
|
5
|
+
ownerEmail?: string;
|
|
6
|
+
/** Bytes du blob — Uint8Array côté Node, Blob côté Web (factory accepte les 2). */
|
|
7
|
+
body: Uint8Array | Blob | ArrayBuffer;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
durationMs: number;
|
|
10
|
+
/** Si non fourni, dérivé du mimeType (`video/*` → video, `audio/*` → audio, sinon image). */
|
|
11
|
+
kind?: MediaKind;
|
|
12
|
+
/** Session multi-take (groupe N takes ensemble). */
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
takeIndex?: number;
|
|
15
|
+
/** Bucket cible (défaut `media-recordings`). */
|
|
16
|
+
bucket?: string;
|
|
17
|
+
/** Métadonnées libres persistées sur la row. */
|
|
18
|
+
metadata?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
/** Résultat de createRecording — row + signedUrl frais. */
|
|
21
|
+
export interface CreateRecordingResult {
|
|
22
|
+
row: MediaRow;
|
|
23
|
+
/** Signed URL pour playback immédiat. TTL configuré via factory.options. */
|
|
24
|
+
signedUrl: string;
|
|
25
|
+
/** Expiration absolute du signed URL (ms epoch). */
|
|
26
|
+
signedUrlExpiresAt: number;
|
|
27
|
+
}
|
|
28
|
+
/** Résultat de getMedia / reissueSignedUrl. */
|
|
29
|
+
export interface MediaWithSignedUrl {
|
|
30
|
+
row: MediaRow;
|
|
31
|
+
signedUrl: string;
|
|
32
|
+
signedUrlExpiresAt: number;
|
|
33
|
+
}
|
|
34
|
+
/** Input du PUT recording — remplace le blob d'une row existante (saveOrUpdate). */
|
|
35
|
+
export interface UpdateRecordingInput {
|
|
36
|
+
/** Nouveaux bytes du blob — écrasent l'objet storage existant (même key). */
|
|
37
|
+
body: Uint8Array | Blob | ArrayBuffer;
|
|
38
|
+
/** MIME optionnel — si absent, on conserve celui de la row. */
|
|
39
|
+
mimeType?: string;
|
|
40
|
+
/** Durée optionnelle. */
|
|
41
|
+
durationMs?: number;
|
|
42
|
+
/** Métadonnées — remplacent celles de la row si fourni. */
|
|
43
|
+
metadata?: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
/** Service principal — surface publique consommée par les route handlers HTTP. */
|
|
46
|
+
export interface MediaService {
|
|
47
|
+
/**
|
|
48
|
+
* Crée une nouvelle row + uploade le blob dans storage + retourne signed URL.
|
|
49
|
+
* Le flux est : insert row(status=uploading) → put storage → update row(status=ready)
|
|
50
|
+
* → sign URL. Si put storage échoue, la row passe à status=failed (audit).
|
|
51
|
+
*/
|
|
52
|
+
createRecording(input: CreateRecordingInput): Promise<CreateRecordingResult>;
|
|
53
|
+
/**
|
|
54
|
+
* Met à jour une row existante : ré-uploade le blob (même bucket/key, écrase)
|
|
55
|
+
* + met à jour size/checksum/mime/durationMs/metadata. Retourne null si la
|
|
56
|
+
* row n'existe pas. Pattern saveOrUpdate : le consumer appelle createRecording
|
|
57
|
+
* pour un nouveau média, updateRecording pour ré-enregistrer un existant.
|
|
58
|
+
*/
|
|
59
|
+
updateRecording(id: string, input: UpdateRecordingInput): Promise<CreateRecordingResult | null>;
|
|
60
|
+
/** Récupère row + signed URL frais. */
|
|
61
|
+
getMedia(id: string): Promise<MediaWithSignedUrl | null>;
|
|
62
|
+
/** Re-génère un signed URL (le précédent peut avoir expiré). */
|
|
63
|
+
reissueSignedUrl(id: string, ttlSec?: number): Promise<MediaWithSignedUrl | null>;
|
|
64
|
+
/** Soft-delete : row.status=deleted + purge blob storage. */
|
|
65
|
+
deleteMedia(id: string): Promise<void>;
|
|
66
|
+
/** Liste paginée pour un tenant. */
|
|
67
|
+
listForAccount(accountId: string, filter?: MediaListFilter): Promise<MediaListResult>;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAExF,wFAAwF;AACxF,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mFAAmF;IACnF,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,WAAW,CAAA;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,6FAA6F;IAC7F,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAED,2DAA2D;AAC3D,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,QAAQ,CAAA;IACb,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAA;IACjB,oDAAoD;IACpD,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AAED,+CAA+C;AAC/C,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,QAAQ,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AAED,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,WAAW,CAAA;IACrC,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yBAAyB;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAED,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAC5E;;;;;OAKG;IACH,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAA;IAC/F,uCAAuC;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAA;IACxD,gEAAgE;IAChE,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAA;IACjF,6DAA6D;IAC7D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,oCAAoC;IACpC,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;CACtF"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,0CAA0C"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
quel est l'intert du mosta-media-server
|
|
2
|
+
|
|
3
|
+
● Bonne question — je m'arrête. L'intérêt de mosta-media-server se résume en 5
|
|
4
|
+
points :
|
|
5
|
+
|
|
6
|
+
1. Découplage peer deps — @mostajs/media actuel est browser-only (React,
|
|
7
|
+
❯ MediaRecorder, IDB). Y ajouter @mostajs/orm + better-sqlite3 + handlers HTTP
|
|
8
|
+
──pollue le bundle client (~+800 kB inutiles à Webpack/Turbopack) et complique
|
|
9
|
+
le tree-shaking pour les consumers Next.js qui n'ont besoin que des composants
|
|
10
|
+
client. C'était d'ailleurs la décision D4 du design (§10).
|
|
11
|
+
2. Une seule implémentation pour N consumers — camera-studio, iquesta
|
|
12
|
+
course-builder, et tout futur app qui voudra persister un recording n'auront
|
|
13
|
+
pas à réécrire le triplet (row Media DB, put blob storage, signed URL). Le
|
|
14
|
+
factory createMedia({ storage, repo }) injecté permet à chaque app de plugger
|
|
15
|
+
SON driver storage et SON repo ORM.
|
|
16
|
+
3. Contrat serveur testable — MediaService est une interface unique
|
|
17
|
+
(createRecording, getMedia, reissueSignedUrl, deleteMedia, listForAccount).
|
|
18
|
+
Mockable trivial pour tests, et son contrat correspond 1:1 aux 4-5 endpoints
|
|
19
|
+
HTTP nécessaires.
|
|
20
|
+
4. Préparation P2 multi-source — quand on attaquera 4.1 smartphone push (D5
|
|
21
|
+
prioritaire), le serveur aura déjà la table Media + endpoints de finalisation.
|
|
22
|
+
Le smartphone publish son flux via WebRTC vers le serveur, le serveur écrit
|
|
23
|
+
dans storage et crée une row Media. Sans le module server, il faudrait refaire
|
|
24
|
+
ce câblage à zéro.
|
|
25
|
+
5. Pivot iquesta course-builder débloqué — la mémoire
|
|
26
|
+
project_iquesta_v4_recording_plan indique que <MediaRecorderModal> + endpoint
|
|
27
|
+
POST /api/admin/media/recordings + <RecordingPreview> + wire-up
|
|
28
|
+
<KindSpecificFields> attendent exactement cette factory. Sans elle, iquesta
|
|
29
|
+
doit dupliquer la logique en local.
|
|
30
|
+
|
|
31
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/media-server",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Server-side factory + schema + service contract for @mostajs/media recordings — pairs with @mostajs/storage for blob persistence and an injected ORM repository for row metadata.",
|
|
5
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
|
+
"license": "AGPL-3.0-or-later",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"docs"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc && npm run fix-esm",
|
|
25
|
+
"fix-esm": "find dist -name '*.js' -exec sed -i -E \"s|from '(\\.{1,2}/[^']+)'(;?)|from '\\1.js'\\2|g\" {} \\; && find dist -name '*.js' -exec sed -i -E \"s|\\.js\\.js|.js|g\" {} \\;",
|
|
26
|
+
"clean": "rm -rf dist"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@mostajs/media": "^2.0.4",
|
|
30
|
+
"@mostajs/storage": "^0.1.2"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"@mostajs/media": { "optional": false },
|
|
34
|
+
"@mostajs/storage": { "optional": false }
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "^5.4.0"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"media",
|
|
41
|
+
"recording",
|
|
42
|
+
"server",
|
|
43
|
+
"storage",
|
|
44
|
+
"mostajs"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|