sanity-plugin-mux-input 2.9.1 → 2.10.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.
@@ -0,0 +1,143 @@
1
+ import {useMemo, useState} from 'react'
2
+ import {
3
+ createHookFromObservableFactory,
4
+ type DocumentStore,
5
+ useClient,
6
+ useDocumentStore,
7
+ } from 'sanity'
8
+
9
+ import {isEmptyOrPlaceholderTitle} from '../util/assetTitlePlaceholder'
10
+ import type {MuxAsset, VideoAssetDocument} from '../util/types'
11
+ import {SANITY_API_VERSION} from './useClient'
12
+ import useMuxAssets from './useMuxAssets'
13
+ import {useSecretsDocumentValues} from './useSecretsDocumentValues'
14
+
15
+ type ResyncState = 'closed' | 'idle' | 'syncing' | 'done' | 'error'
16
+
17
+ export type MatchedAsset = {
18
+ sanityDoc: VideoAssetDocument
19
+ muxAsset: MuxAsset | undefined
20
+ muxTitle: string | undefined
21
+ currentTitle: string | undefined
22
+ }
23
+
24
+ export default function useResyncMuxMetadata() {
25
+ const documentStore = useDocumentStore()
26
+ const client = useClient({
27
+ apiVersion: SANITY_API_VERSION,
28
+ })
29
+
30
+ const [sanityAssets, sanityAssetsLoading] = useSanityAssets(documentStore)
31
+
32
+ const secretDocumentValues = useSecretsDocumentValues()
33
+ const hasSecrets = !!secretDocumentValues.value.secrets?.secretKey
34
+
35
+ const [resyncError, setResyncError] = useState<unknown>()
36
+ const [resyncState, setResyncState] = useState<ResyncState>('closed')
37
+ const dialogOpen = resyncState !== 'closed'
38
+
39
+ const muxAssets = useMuxAssets({
40
+ client,
41
+ enabled: hasSecrets && dialogOpen,
42
+ })
43
+
44
+ const matchedAssets = useMemo(() => {
45
+ return sanityAssets && muxAssets.data
46
+ ? sanityAssets.map((sanityDoc) => {
47
+ const muxAsset = muxAssets.data?.find(
48
+ (m) => m.id === sanityDoc.assetId || m.id === sanityDoc.data?.id
49
+ )
50
+ return {
51
+ sanityDoc,
52
+ muxAsset,
53
+ muxTitle: muxAsset?.meta?.title,
54
+ currentTitle: sanityDoc.filename,
55
+ }
56
+ })
57
+ : undefined
58
+ }, [sanityAssets, muxAssets.data])
59
+
60
+ const closeDialog = () => {
61
+ if (resyncState !== 'syncing') setResyncState('closed')
62
+ }
63
+
64
+ const openDialog = () => {
65
+ if (resyncState === 'closed') setResyncState('idle')
66
+ }
67
+
68
+ async function syncAllVideos() {
69
+ if (!matchedAssets) return
70
+
71
+ setResyncState('syncing')
72
+
73
+ try {
74
+ const tx = client.transaction()
75
+
76
+ matchedAssets.forEach((matched) => {
77
+ // Update all videos with the Mux title, even if it's undefined/empty
78
+ const newTitle = matched.muxTitle || ''
79
+ tx.patch(matched.sanityDoc._id, {set: {filename: newTitle}})
80
+ })
81
+
82
+ await tx.commit({returnDocuments: false})
83
+ setResyncState('done')
84
+ } catch (error) {
85
+ setResyncState('error')
86
+ setResyncError(error)
87
+ }
88
+ }
89
+
90
+ async function syncOnlyEmpty() {
91
+ if (!matchedAssets) return
92
+
93
+ setResyncState('syncing')
94
+
95
+ try {
96
+ const tx = client.transaction()
97
+
98
+ matchedAssets.forEach((matched) => {
99
+ // Only update if the current title is empty or has the placeholder format
100
+ // AND there's a new title available from Mux
101
+ if (
102
+ matched.muxAsset &&
103
+ matched.muxTitle &&
104
+ isEmptyOrPlaceholderTitle(matched.currentTitle, matched.muxAsset.id)
105
+ ) {
106
+ tx.patch(matched.sanityDoc._id, {set: {filename: matched.muxTitle}})
107
+ }
108
+ })
109
+
110
+ await tx.commit({returnDocuments: false})
111
+ setResyncState('done')
112
+ } catch (error) {
113
+ setResyncState('error')
114
+ setResyncError(error)
115
+ }
116
+ }
117
+
118
+ return {
119
+ sanityAssetsLoading,
120
+ closeDialog,
121
+ dialogOpen,
122
+ resyncState,
123
+ resyncError,
124
+ hasSecrets,
125
+ syncAllVideos,
126
+ syncOnlyEmpty,
127
+ matchedAssets,
128
+ muxAssets,
129
+ openDialog,
130
+ }
131
+ }
132
+
133
+ const useSanityAssets = createHookFromObservableFactory<VideoAssetDocument[], DocumentStore>(
134
+ (documentStore) => {
135
+ return documentStore.listenQuery(
136
+ /* groq */ `*[_type == "mux.videoAsset"]`,
137
+ {},
138
+ {
139
+ apiVersion: SANITY_API_VERSION,
140
+ }
141
+ )
142
+ }
143
+ )
@@ -0,0 +1,31 @@
1
+ import {truncateString} from 'sanity'
2
+
3
+ /**
4
+ * Generates a placeholder title for a Mux asset when no title is available.
5
+ * This format is used when importing assets that don't have a title in Mux.
6
+ *
7
+ * @param assetId - The Mux asset ID
8
+ * @returns A placeholder title in the format "Asset #[truncated-id]"
9
+ */
10
+ export function generateAssetPlaceholder(assetId: string): string {
11
+ return `Asset #${truncateString(assetId, 15)}`
12
+ }
13
+
14
+ /**
15
+ * Checks if a filename is empty or has the placeholder format.
16
+ * This is used to determine if a video title should be updated during metadata sync.
17
+ *
18
+ * @param filename - The current filename/title of the video
19
+ * @param assetId - The Mux asset ID to check against
20
+ * @returns true if the filename is empty or matches the placeholder format
21
+ */
22
+ export function isEmptyOrPlaceholderTitle(filename: string | undefined, assetId: string): boolean {
23
+ // Check if filename is empty/undefined
24
+ if (!filename || filename.trim() === '') {
25
+ return true
26
+ }
27
+
28
+ // Check if filename matches the placeholder format for this asset
29
+ const placeholder = generateAssetPlaceholder(assetId)
30
+ return filename === placeholder
31
+ }