sanity-plugin-mux-input 2.17.0 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +80 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +80 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/actions/assets.ts +7 -1
- package/src/actions/secrets.ts +6 -0
- package/src/actions/upload.ts +10 -4
- package/src/components/AddCaptionDialog.tsx +28 -9
- package/src/components/EditCaptionDialog.tsx +4 -2
- package/src/hooks/useMuxPolling.ts +2 -0
- package/src/util/formatDriveShareLink.ts +64 -0
- package/src/util/pluginVersion.ts +1 -0
package/package.json
CHANGED
package/src/actions/assets.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {SanityClient} from 'sanity'
|
|
2
2
|
|
|
3
|
+
import {PLUGIN_VERSION_QUERY} from '../util/pluginVersion'
|
|
3
4
|
import type {MuxAsset, VideoAssetDocument} from '../util/types'
|
|
4
5
|
|
|
5
6
|
export function deleteAssetOnMux(client: SanityClient, assetId: string) {
|
|
@@ -8,6 +9,7 @@ export function deleteAssetOnMux(client: SanityClient, assetId: string) {
|
|
|
8
9
|
url: `/addons/mux/assets/${dataset}/${assetId}`,
|
|
9
10
|
withCredentials: true,
|
|
10
11
|
method: 'DELETE',
|
|
12
|
+
query: PLUGIN_VERSION_QUERY,
|
|
11
13
|
})
|
|
12
14
|
}
|
|
13
15
|
|
|
@@ -45,6 +47,7 @@ export function getAsset(client: SanityClient, assetId: string) {
|
|
|
45
47
|
url: `/addons/mux/assets/${dataset}/data/${assetId}`,
|
|
46
48
|
withCredentials: true,
|
|
47
49
|
method: 'GET',
|
|
50
|
+
query: PLUGIN_VERSION_QUERY,
|
|
48
51
|
})
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -66,7 +69,7 @@ export function listAssets(
|
|
|
66
69
|
url: `/addons/mux/assets/${dataset}/data/list`,
|
|
67
70
|
withCredentials: true,
|
|
68
71
|
method: 'GET',
|
|
69
|
-
query,
|
|
72
|
+
query: {...query, ...PLUGIN_VERSION_QUERY},
|
|
70
73
|
})
|
|
71
74
|
}
|
|
72
75
|
|
|
@@ -99,6 +102,7 @@ export function addTextTrackFromUrl(
|
|
|
99
102
|
headers: {
|
|
100
103
|
'Content-Type': 'application/json',
|
|
101
104
|
},
|
|
105
|
+
query: PLUGIN_VERSION_QUERY,
|
|
102
106
|
})
|
|
103
107
|
}
|
|
104
108
|
|
|
@@ -130,6 +134,7 @@ export function generateSubtitles(
|
|
|
130
134
|
headers: {
|
|
131
135
|
'Content-Type': 'application/json',
|
|
132
136
|
},
|
|
137
|
+
query: PLUGIN_VERSION_QUERY,
|
|
133
138
|
})
|
|
134
139
|
}
|
|
135
140
|
|
|
@@ -142,5 +147,6 @@ export function deleteTextTrack(client: SanityClient, assetId: string, trackId:
|
|
|
142
147
|
url: `/addons/mux/assets/${dataset}/${assetId}/tracks/${trackId}`,
|
|
143
148
|
withCredentials: true,
|
|
144
149
|
method: 'DELETE',
|
|
150
|
+
query: PLUGIN_VERSION_QUERY,
|
|
145
151
|
})
|
|
146
152
|
}
|
package/src/actions/secrets.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {defer} from 'rxjs'
|
|
2
2
|
import type {SanityClient} from 'sanity'
|
|
3
3
|
|
|
4
|
+
import {PLUGIN_VERSION_QUERY} from '../util/pluginVersion'
|
|
5
|
+
|
|
4
6
|
interface SecretsDocument {
|
|
5
7
|
_id: 'secrets.mux'
|
|
6
8
|
_type: 'mux.apiKey'
|
|
@@ -46,6 +48,7 @@ export async function createSigningKeys(client: SanityClient) {
|
|
|
46
48
|
url: `/addons/mux/signing-keys/${dataset}`,
|
|
47
49
|
withCredentials: true,
|
|
48
50
|
method: 'POST',
|
|
51
|
+
query: PLUGIN_VERSION_QUERY,
|
|
49
52
|
})
|
|
50
53
|
return res
|
|
51
54
|
} catch (error: any) {
|
|
@@ -64,6 +67,7 @@ export function testSecrets(client: SanityClient) {
|
|
|
64
67
|
url: `/addons/mux/secrets/${dataset}/test`,
|
|
65
68
|
withCredentials: true,
|
|
66
69
|
method: 'GET',
|
|
70
|
+
query: PLUGIN_VERSION_QUERY,
|
|
67
71
|
})
|
|
68
72
|
}
|
|
69
73
|
|
|
@@ -82,6 +86,7 @@ export async function haveValidSigningKeys(
|
|
|
82
86
|
url: `/addons/mux/signing-keys/${dataset}/${signingKeyId}`,
|
|
83
87
|
withCredentials: true,
|
|
84
88
|
method: 'GET',
|
|
89
|
+
query: PLUGIN_VERSION_QUERY,
|
|
85
90
|
})
|
|
86
91
|
//
|
|
87
92
|
// if this signing key is valid it will return { data: { id: 'xxxx' } }
|
|
@@ -100,6 +105,7 @@ export function testSecretsObservable(client: SanityClient) {
|
|
|
100
105
|
url: `/addons/mux/secrets/${dataset}/test`,
|
|
101
106
|
withCredentials: true,
|
|
102
107
|
method: 'GET',
|
|
108
|
+
query: PLUGIN_VERSION_QUERY,
|
|
103
109
|
})
|
|
104
110
|
)
|
|
105
111
|
}
|
package/src/actions/upload.ts
CHANGED
|
@@ -4,6 +4,8 @@ import {catchError, mergeMap, mergeMapTo, switchMap} from 'rxjs/operators'
|
|
|
4
4
|
import type {SanityClient} from 'sanity'
|
|
5
5
|
|
|
6
6
|
import {createUpChunkObservable} from '../clients/upChunkObservable'
|
|
7
|
+
import {formatDriveShareLink} from '../util/formatDriveShareLink'
|
|
8
|
+
import {PLUGIN_VERSION_QUERY} from '../util/pluginVersion'
|
|
7
9
|
import {roundPxString} from '../util/roundPxString'
|
|
8
10
|
import type {MuxAsset, MuxNewAssetSettings, WatermarkConfig} from '../util/types'
|
|
9
11
|
import {getAsset} from './assets'
|
|
@@ -41,6 +43,7 @@ export function cancelUpload(client: SanityClient, uuid: string) {
|
|
|
41
43
|
url: `/addons/mux/uploads/${client.config().dataset}/${uuid}`,
|
|
42
44
|
withCredentials: true,
|
|
43
45
|
method: 'DELETE',
|
|
46
|
+
query: PLUGIN_VERSION_QUERY,
|
|
44
47
|
})
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -85,7 +88,7 @@ export function uploadUrl({
|
|
|
85
88
|
'MUX-Proxy-UUID': uuid,
|
|
86
89
|
'Content-Type': 'application/json',
|
|
87
90
|
},
|
|
88
|
-
query,
|
|
91
|
+
query: {...query, ...PLUGIN_VERSION_QUERY},
|
|
89
92
|
})
|
|
90
93
|
).pipe(
|
|
91
94
|
mergeMap((result) => {
|
|
@@ -152,6 +155,7 @@ export function uploadFile({
|
|
|
152
155
|
'Content-Type': 'application/json',
|
|
153
156
|
},
|
|
154
157
|
body,
|
|
158
|
+
query: PLUGIN_VERSION_QUERY,
|
|
155
159
|
})
|
|
156
160
|
).pipe(
|
|
157
161
|
mergeMap((result) => {
|
|
@@ -204,6 +208,7 @@ export function getUpload(client: SanityClient, assetId: string) {
|
|
|
204
208
|
url: `/addons/mux/uploads/${dataset}/${assetId}`,
|
|
205
209
|
withCredentials: true,
|
|
206
210
|
method: 'GET',
|
|
211
|
+
query: PLUGIN_VERSION_QUERY,
|
|
207
212
|
})
|
|
208
213
|
}
|
|
209
214
|
|
|
@@ -280,17 +285,18 @@ export function testUrl(url: string): Observable<string> {
|
|
|
280
285
|
if (typeof url !== 'string') {
|
|
281
286
|
return throwError(error)
|
|
282
287
|
}
|
|
283
|
-
|
|
288
|
+
let formattedUrl = url.trim()
|
|
289
|
+
formattedUrl = formatDriveShareLink(formattedUrl)
|
|
284
290
|
let parsed
|
|
285
291
|
try {
|
|
286
|
-
parsed = new URL(
|
|
292
|
+
parsed = new URL(formattedUrl)
|
|
287
293
|
} catch (err) {
|
|
288
294
|
return throwError(error)
|
|
289
295
|
}
|
|
290
296
|
if (parsed && !parsed.protocol.match(/http:|https:/)) {
|
|
291
297
|
return throwError(error)
|
|
292
298
|
}
|
|
293
|
-
return of(
|
|
299
|
+
return of(formattedUrl)
|
|
294
300
|
}
|
|
295
301
|
|
|
296
302
|
function optionsFromFile(opts: {preserveFilename?: boolean}, file: File) {
|
|
@@ -18,6 +18,7 @@ import {useId, useRef, useState} from 'react'
|
|
|
18
18
|
|
|
19
19
|
import {addTextTrackFromUrl, generateSubtitles, getAsset} from '../actions/assets'
|
|
20
20
|
import {useClient} from '../hooks/useClient'
|
|
21
|
+
import {addKeysToMuxData} from '../util/addKeysToMuxData'
|
|
21
22
|
import {extractErrorMessage, pollTrackStatus} from '../util/textTracks'
|
|
22
23
|
import {type MuxTextTrack, SUPPORTED_MUX_LANGUAGES, type VideoAssetDocument} from '../util/types'
|
|
23
24
|
|
|
@@ -60,6 +61,20 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
60
61
|
return assetDocument.url
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
const refreshAssetData = async () => {
|
|
65
|
+
if (!asset._id || !asset.assetId) return
|
|
66
|
+
try {
|
|
67
|
+
const latestAssetData = await getAsset(client, asset.assetId)
|
|
68
|
+
const dataWithKeys = addKeysToMuxData(latestAssetData.data)
|
|
69
|
+
await client
|
|
70
|
+
.patch(asset._id)
|
|
71
|
+
.set({data: dataWithKeys, status: latestAssetData.data.status})
|
|
72
|
+
.commit({returnDocuments: false})
|
|
73
|
+
} catch (refreshError) {
|
|
74
|
+
console.error('Failed to refresh asset data:', refreshError)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
const handleAddTrackFromUrl = async () => {
|
|
64
79
|
if (!asset.assetId) {
|
|
65
80
|
throw new Error('Asset ID is required')
|
|
@@ -75,9 +90,9 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
75
90
|
vttUrlToUse = await uploadVttFile(selectedFile)
|
|
76
91
|
} catch (uploadError) {
|
|
77
92
|
toast.push({
|
|
78
|
-
title: 'Failed to upload
|
|
93
|
+
title: 'Failed to upload caption file',
|
|
79
94
|
status: 'error',
|
|
80
|
-
description: 'Could not upload the
|
|
95
|
+
description: 'Could not upload the caption file to Sanity. Please try again.',
|
|
81
96
|
})
|
|
82
97
|
setIsSubmitting(false)
|
|
83
98
|
throw uploadError
|
|
@@ -95,7 +110,7 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
95
110
|
assetId: asset.assetId,
|
|
96
111
|
trackName: trimmedName,
|
|
97
112
|
trackLanguageCode: trimmedLanguageCode,
|
|
98
|
-
onTrackErrored: (track) => {
|
|
113
|
+
onTrackErrored: async (track) => {
|
|
99
114
|
const errorMessage =
|
|
100
115
|
track.error?.messages?.[0] ||
|
|
101
116
|
track.error?.type ||
|
|
@@ -105,6 +120,7 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
105
120
|
status: 'error',
|
|
106
121
|
description: errorMessage,
|
|
107
122
|
})
|
|
123
|
+
await refreshAssetData()
|
|
108
124
|
onAdd(track)
|
|
109
125
|
onClose()
|
|
110
126
|
},
|
|
@@ -132,11 +148,13 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
132
148
|
description:
|
|
133
149
|
'The track was created and is being processed. It will appear in the list shortly.',
|
|
134
150
|
})
|
|
151
|
+
await refreshAssetData()
|
|
135
152
|
onAdd(result.track)
|
|
136
153
|
onClose()
|
|
137
154
|
return
|
|
138
155
|
}
|
|
139
156
|
|
|
157
|
+
await refreshAssetData()
|
|
140
158
|
toast.push({
|
|
141
159
|
title: 'Caption track added',
|
|
142
160
|
status: 'success',
|
|
@@ -195,9 +213,9 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
195
213
|
if (!isAutogenerated) {
|
|
196
214
|
if (!selectedFile && !vttUrl.trim()) {
|
|
197
215
|
toast.push({
|
|
198
|
-
title: '
|
|
216
|
+
title: 'Caption file or URL required',
|
|
199
217
|
status: 'error',
|
|
200
|
-
description: 'Please select a VTT file or enter a
|
|
218
|
+
description: 'Please select a VTT or SRT file or enter a caption file URL',
|
|
201
219
|
})
|
|
202
220
|
return
|
|
203
221
|
}
|
|
@@ -209,7 +227,8 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
209
227
|
toast.push({
|
|
210
228
|
title: 'Invalid URL',
|
|
211
229
|
status: 'error',
|
|
212
|
-
description:
|
|
230
|
+
description:
|
|
231
|
+
'Please enter a valid URL (e.g., https://example.com/subtitles.vtt or subtitles.srt)',
|
|
213
232
|
})
|
|
214
233
|
return
|
|
215
234
|
}
|
|
@@ -303,7 +322,7 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
303
322
|
<input
|
|
304
323
|
ref={fileInputRef}
|
|
305
324
|
type="file"
|
|
306
|
-
accept=".vtt,text/vtt"
|
|
325
|
+
accept=".vtt,text/vtt,.srt,application/x-subrip"
|
|
307
326
|
style={{display: 'none'}}
|
|
308
327
|
onChange={(e) => {
|
|
309
328
|
if (e.target.files && e.target.files.length > 0 && !isSubmitting) {
|
|
@@ -314,10 +333,10 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
314
333
|
/>
|
|
315
334
|
</Card>
|
|
316
335
|
<Text size={1} muted style={{textAlign: 'center'}}>
|
|
317
|
-
Or enter the
|
|
336
|
+
Or enter the caption file URL
|
|
318
337
|
</Text>
|
|
319
338
|
<Stack space={2}>
|
|
320
|
-
<Label htmlFor="vtt-url">
|
|
339
|
+
<Label htmlFor="vtt-url">Caption File URL (.vtt or .srt)</Label>
|
|
321
340
|
<TextInput
|
|
322
341
|
id="vtt-url"
|
|
323
342
|
placeholder="https://example.com/subtitles.vtt"
|
|
@@ -17,6 +17,7 @@ import {useEffect, useId, useRef, useState} from 'react'
|
|
|
17
17
|
|
|
18
18
|
import {addTextTrackFromUrl, deleteTextTrack, getAsset} from '../actions/assets'
|
|
19
19
|
import {useClient} from '../hooks/useClient'
|
|
20
|
+
import {addKeysToMuxData} from '../util/addKeysToMuxData'
|
|
20
21
|
import {generateJwt} from '../util/generateJwt'
|
|
21
22
|
import {getPlaybackId} from '../util/getPlaybackPolicy'
|
|
22
23
|
import {getPlaybackPolicy} from '../util/getPlaybackPolicy'
|
|
@@ -125,10 +126,11 @@ export default function EditCaptionDialog({asset, track, onUpdate, onClose}: Pro
|
|
|
125
126
|
if (!asset._id || !asset.assetId) return
|
|
126
127
|
try {
|
|
127
128
|
const latestAssetData = await getAsset(client, asset.assetId)
|
|
129
|
+
const dataWithKeys = addKeysToMuxData(latestAssetData.data)
|
|
128
130
|
await client
|
|
129
131
|
.patch(asset._id)
|
|
130
|
-
.set({data:
|
|
131
|
-
.commit()
|
|
132
|
+
.set({data: dataWithKeys, status: latestAssetData.data.status})
|
|
133
|
+
.commit({returnDocuments: false})
|
|
132
134
|
} catch (refreshError) {
|
|
133
135
|
console.error('Failed to refresh asset data:', refreshError)
|
|
134
136
|
}
|
|
@@ -3,6 +3,7 @@ import {useDataset, useProjectId} from 'sanity'
|
|
|
3
3
|
import useSWR from 'swr'
|
|
4
4
|
|
|
5
5
|
import {useClient} from '../hooks/useClient'
|
|
6
|
+
import {PLUGIN_VERSION_QUERY} from '../util/pluginVersion'
|
|
6
7
|
import type {MuxAsset, VideoAssetDocument} from '../util/types'
|
|
7
8
|
|
|
8
9
|
// Poll MUX if it's preparing the main document or its own static renditions
|
|
@@ -39,6 +40,7 @@ export const useMuxPolling = (asset?: VideoAssetDocument) => {
|
|
|
39
40
|
url: `/addons/mux/assets/${dataset}/data/${asset!.assetId}`,
|
|
40
41
|
withCredentials: true,
|
|
41
42
|
method: 'GET',
|
|
43
|
+
query: PLUGIN_VERSION_QUERY,
|
|
42
44
|
})
|
|
43
45
|
client.patch(asset!._id!).set({status: data.status, data}).commit({returnDocuments: false})
|
|
44
46
|
},
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format Google Drive share links as Google Drive export links.
|
|
3
|
+
* Supported formats:
|
|
4
|
+
* - https://drive.google.com/uc?id=<ID>...
|
|
5
|
+
* - https://drive.google.com/open?id=<ID>...
|
|
6
|
+
* - https://drive.google.com/file/d/<ID>...
|
|
7
|
+
* - https://drive.google.com/folder/<ID>...
|
|
8
|
+
*
|
|
9
|
+
* @param url Google Drive share link to format.
|
|
10
|
+
* @returns Google Drive export link (URL passthrough if not share link).
|
|
11
|
+
*/
|
|
12
|
+
export function formatDriveShareLink(url: string): string {
|
|
13
|
+
// Export link formatter.
|
|
14
|
+
const formatExportLink = (id: string) => {
|
|
15
|
+
return `https://drive.google.com/uc?export=download&id=${id}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// URL formatting.
|
|
19
|
+
try {
|
|
20
|
+
// Parse URL.
|
|
21
|
+
const trimmed = url.trim()
|
|
22
|
+
const parsed = new URL(trimmed)
|
|
23
|
+
|
|
24
|
+
// Enforce strict host name.
|
|
25
|
+
if (parsed.hostname !== 'drive.google.com') {
|
|
26
|
+
throw new Error('URL is not from Google Drive.')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Look for ID in search parameters.
|
|
30
|
+
const id = parsed.searchParams.get('id') || ''
|
|
31
|
+
if (id.length) {
|
|
32
|
+
return formatExportLink(id)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Look for ID in path name.
|
|
36
|
+
const path = parsed.pathname.split('/') || []
|
|
37
|
+
|
|
38
|
+
// Path is /file/d/<ID>...
|
|
39
|
+
if (path.includes('file') && path.includes('d')) {
|
|
40
|
+
const index =
|
|
41
|
+
path.findIndex((value: string) => {
|
|
42
|
+
return value === 'd'
|
|
43
|
+
}) + 1
|
|
44
|
+
const fileId = path.at(index) || ''
|
|
45
|
+
return formatExportLink(fileId)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Path is /folder/<ID>...
|
|
49
|
+
if (path.includes('folders')) {
|
|
50
|
+
const index =
|
|
51
|
+
path.findIndex((value: string) => {
|
|
52
|
+
return value === 'folders'
|
|
53
|
+
}) + 1
|
|
54
|
+
const folderId = path.at(index) || ''
|
|
55
|
+
return formatExportLink(folderId)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// URL not recognized.
|
|
59
|
+
throw new Error('URL was not recognized.')
|
|
60
|
+
} catch {
|
|
61
|
+
// URL passthrough by default.
|
|
62
|
+
return url
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PLUGIN_VERSION_QUERY = {sanityVersion: process.env.PKG_VERSION!}
|