nuxt-media-library 0.0.61 → 0.0.62

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 CHANGED
@@ -188,8 +188,85 @@ export default createMLCatchAllProxy({
188
188
  and, when `serviceUploads.proxy` is on, the service-upload pass-through.
189
189
  Your catch-all handles everything else under `/api/media-library`.
190
190
 
191
+ ### Showing media on public pages (embedded media)
192
+
193
+ Public pages never resolve an asset id. Each service puts the media into its
194
+ own response (Python: `pkg_media`'s `MediaEmbedder`, or `StrawberryMedia` for
195
+ GraphQL), having decided who may see what. The page renders the fields it gets:
196
+
197
+ ```ts
198
+ interface EmbeddedMedia { // from the service's GraphQL schema (codegen)
199
+ assetId: string
200
+ kind: 'image' | 'video' | 'audio' | 'document' | 'other'
201
+ url: string | null // the file; null when denied or not ready
202
+ poster: string | null // the one image to show; set even when denied
203
+ sources: { url: string, width: number, height?: number | null }[] // poster sizes
204
+ streams: { hls?: string | null, dash?: string | null } | null
205
+ width?: number | null; height?: number | null; alt?: string | null
206
+ expiresIn: number | null // seconds until the first link expires; null = never
207
+ public: boolean // every link tokenless (cache-safe)
208
+ }
209
+ ```
210
+
211
+ The module auto-imports the helpers that read it (a structural type,
212
+ `MLEmbeddedMedia`, so any codegen type fits):
213
+
214
+ | Helper | Gives |
215
+ |---|---|
216
+ | `mlMediaPoster(media, fallback?)` | the image to show, else `fallback` |
217
+ | `mlMediaSrcset(media)` | `srcset` from `media.sources`, or `undefined` |
218
+ | `mlPlayableSource(media, { preferFile? })` | `{ src, type: 'hls' \| 'progressive' }`: the HLS ladder, else the file; `null` when locked or not ready |
219
+ | `mlIsPlayableVideo(media)` | a video the viewer can play now |
220
+ | `useMLMediaRenewal(media, renew, { marginSeconds? })` | calls `renew` 5 minutes (default) before tokened links expire; browser only |
221
+
222
+ ```vue
223
+ <script setup lang="ts">
224
+ const { data: item } = await useAsyncData('article', fetchArticle)
225
+ // Re-fetch before the links expire and swap in only `media`.
226
+ useMLMediaRenewal(() => item.value?.media, async () => {
227
+ const fresh = await fetchArticle()
228
+ if (item.value && fresh?.media) item.value = { ...item.value, media: fresh.media }
229
+ })
230
+ const source = computed(() => mlPlayableSource(item.value?.media))
231
+ </script>
232
+
233
+ <template>
234
+ <!-- `sizes` is the box's widest rendered width. -->
235
+ <img
236
+ :src="mlMediaPoster(item?.media, '/placeholder.jpg')"
237
+ :srcset="mlMediaSrcset(item?.media)"
238
+ sizes="(min-width: 1024px) 420px, 100vw"
239
+ alt=""
240
+ loading="lazy"
241
+ >
242
+ <!-- A video: the app's own player (hls.js for type 'hls'). -->
243
+ <AppVideoPlayer v-if="source" :src="source.src" :type="source.type" :poster="item?.media?.poster" />
244
+ </template>
245
+ ```
246
+
247
+ - **Locked or missing:** `url` and `streams` are null; show `poster` (and a
248
+ paywall). No call to the media library can do better.
249
+ - **Renewal:** tokened links expire (`expiresIn`). `useMLMediaRenewal` fetches the
250
+ record again before then; swap in only the new `media` and resume the player at
251
+ its playhead (key the player on its source).
252
+ - **SEO:** pass `media.poster` (and the service's embedded share image) to your
253
+ SEO helper as URLs; the HTML rendered on the server then carries them.
254
+ - **Caching:** a response with a tokened link is per user (`public: false`);
255
+ don't cache it at the edge.
256
+ - `useMLAssetPreview`, `<MLPreviewProvider>` and `v-asset` resolve ids in the
257
+ browser through `/delivery`. They are **deprecated** (a warning in dev) and go
258
+ with `/delivery`; so do read grants (below).
259
+
260
+ Design and rulings: [`docs/design/embedded-media.md`](docs/design/embedded-media.md).
261
+ Moving a service and its layer over, step by step:
262
+ [`docs/design/embedded-media-migration-guide.md`](docs/design/embedded-media-migration-guide.md).
263
+
191
264
  ### Showing assets of several services: read grants
192
265
 
266
+ > **Deprecated** with `/delivery`: public pages show embedded media instead
267
+ > (see above), and no grant is needed.
268
+
269
+
193
270
  Every link comes from the media library; each service decides only for its
194
271
  own assets by returning a read grant (`mlg_…`, e.g. the Python client's
195
272
  `client.grants.read(ids)`) with the data that references them. Hand the
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "media-library",
3
3
  "configKey": "mediaLibrary",
4
- "version": "0.0.61",
4
+ "version": "0.0.62",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { defineNuxtModule, createResolver, addImportsDir, addComponentsDir, addServerHandler, useLogger, addPlugin } from '@nuxt/kit';
3
+ import { defineNuxtModule, createResolver, addImportsDir, addImports, addComponentsDir, addServerHandler, useLogger, addPlugin } from '@nuxt/kit';
4
4
  import { browserHeaders } from '../dist/runtime/utils/credentialHeaders.js';
5
5
  import { normalizeBackends } from '../dist/runtime/utils/backends.js';
6
6
  export * from '../dist/runtime/types/resourcePolicy.js';
@@ -53,6 +53,10 @@ const module$1 = defineNuxtModule({
53
53
  const resolver = createResolver(import.meta.url);
54
54
  const runtimeDir = resolver.resolve("./runtime");
55
55
  addImportsDir(resolver.resolve("runtime/composables"));
56
+ addImports(["mlMediaPoster", "mlMediaSrcset", "mlPlayableSource", "mlIsPlayableVideo"].map((name) => ({
57
+ name,
58
+ from: resolver.resolve("runtime/utils/embeddedMedia")
59
+ })));
56
60
  nuxt.options.build.transpile ||= [];
57
61
  nuxt.options.build.transpile.push(runtimeDir);
58
62
  const existingPrivate = nuxt.options.runtimeConfig.mediaLibrary || {};
@@ -1,11 +1,13 @@
1
1
  <script setup>
2
2
  import { computed, watch } from "vue";
3
3
  import { useMLAssetPreview } from "../composables/useMLAssetPreview";
4
+ import { warnDeprecated } from "../utils/deprecation";
4
5
  const props = defineProps({
5
6
  assetId: { type: [String, null], required: false },
6
7
  fallbackSrc: { type: [String, null], required: false },
7
8
  autoRegister: { type: Boolean, required: false, default: true }
8
9
  });
10
+ warnDeprecated("<MLPreviewProvider>");
9
11
  const assetsStore = useMLAssetPreview();
10
12
  const asset = computed(
11
13
  () => props.assetId ? assetsStore.getAsset(props.assetId) : void 0
@@ -6,6 +6,7 @@ declare function markBroken(id?: string | null): void;
6
6
  declare function clearBroken(id?: string | null): void;
7
7
  declare function ensureAssets(ids: string[]): Promise<void>;
8
8
  declare function register(id?: string | null): void;
9
+ /** @deprecated Show the service's embedded media instead (see `warnDeprecated`). */
9
10
  export declare const useMLAssetPreview: () => {
10
11
  assets: import("vue").Ref<Record<string, AssetPreview>, Record<string, AssetPreview>>;
11
12
  pending: import("vue").Ref<Set<string> & Omit<Set<string>, keyof Set<any>>, Set<string> | (Set<string> & Omit<Set<string>, keyof Set<any>>)>;
@@ -3,6 +3,7 @@ import { $fetch } from "ofetch";
3
3
  import { mediaGrantHeaders } from "./useMLMediaGrants.js";
4
4
  import { onMediaGrantsAdded } from "../utils/grants/listeners.js";
5
5
  import { streamUrlsStale } from "../utils/streamUrls.js";
6
+ import { warnDeprecated } from "../utils/deprecation.js";
6
7
  const assets = ref({});
7
8
  const pending = ref(/* @__PURE__ */ new Set());
8
9
  const broken = ref(/* @__PURE__ */ new Set());
@@ -159,6 +160,7 @@ onMediaGrantsAdded(() => {
159
160
  if (import.meta.client) scheduleFetch();
160
161
  });
161
162
  export const useMLAssetPreview = () => {
163
+ warnDeprecated("useMLAssetPreview()");
162
164
  return {
163
165
  assets,
164
166
  pending,
@@ -0,0 +1,23 @@
1
+ import { type MaybeRefOrGetter } from 'vue';
2
+ import type { MLEmbeddedMedia } from '../types/embeddedMedia.js';
3
+ export interface UseMLMediaRenewalOptions {
4
+ /** Seconds before expiry to renew. Default 300. */
5
+ marginSeconds?: number;
6
+ }
7
+ /**
8
+ * The renewal loop without the client guard: watches `media` and calls
9
+ * `renew` shortly before its links expire. Returns a stop function. Prefer
10
+ * `useMLMediaRenewal`, which runs this in the browser only.
11
+ */
12
+ export declare function watchMLMediaRenewal(media: MaybeRefOrGetter<MLEmbeddedMedia | null | undefined>, renew: () => Promise<unknown>, { marginSeconds }?: UseMLMediaRenewalOptions): () => void;
13
+ /**
14
+ * Keeps embedded media links alive: tokened links expire (`expiresIn`), so
15
+ * shortly before they do this calls `renew`, which re-fetches the record and
16
+ * swaps in its fresh `media`. Tokenless media (`expiresIn: null`) is left
17
+ * alone. Browser only; stops with the calling scope.
18
+ *
19
+ * Swap only `media` in `renew` rather than refreshing the whole page's data,
20
+ * so the page does not fall back to its loading state mid-playback; a player
21
+ * keyed on its source resumes at its playhead.
22
+ */
23
+ export declare function useMLMediaRenewal(media: MaybeRefOrGetter<MLEmbeddedMedia | null | undefined>, renew: () => Promise<unknown>, options?: UseMLMediaRenewalOptions): void;
@@ -0,0 +1,27 @@
1
+ import { onScopeDispose, toValue, watch } from "vue";
2
+ import { refreshDelay } from "../utils/streamUrls.js";
3
+ const DEFAULT_MARGIN_SECONDS = 300;
4
+ export function watchMLMediaRenewal(media, renew, { marginSeconds = DEFAULT_MARGIN_SECONDS } = {}) {
5
+ let timer = null;
6
+ const clear = () => {
7
+ if (timer) clearTimeout(timer);
8
+ timer = null;
9
+ };
10
+ const stopWatch = watch(() => toValue(media), (current) => {
11
+ clear();
12
+ const now = Date.now();
13
+ const delay = refreshDelay(current?.expiresIn, now, now, marginSeconds);
14
+ if (delay == null) return;
15
+ timer = setTimeout(() => {
16
+ renew().catch((err) => console.warn("[nuxt-media-library] media renewal failed:", err));
17
+ }, delay);
18
+ }, { immediate: true });
19
+ return () => {
20
+ stopWatch();
21
+ clear();
22
+ };
23
+ }
24
+ export function useMLMediaRenewal(media, renew, options = {}) {
25
+ if (!import.meta.client) return;
26
+ onScopeDispose(watchMLMediaRenewal(media, renew, options));
27
+ }
@@ -25,6 +25,9 @@ export * from './utils/assetFormat.js';
25
25
  export * from './utils/fileAccept.js';
26
26
  export * from './utils/processingSteps.js';
27
27
  export * from './utils/streamUrls.js';
28
+ export * from './utils/embeddedMedia.js';
29
+ export * from './types/embeddedMedia.js';
30
+ export * from './composables/useMLMediaRenewal.js';
28
31
  export * from './utils/webAppDeploys.js';
29
32
  export * from './composables/useMLWebAppDeploys.js';
30
33
  export * from './composables/useMLMediaMeta.js';
@@ -25,6 +25,9 @@ export * from "./utils/assetFormat.js";
25
25
  export * from "./utils/fileAccept.js";
26
26
  export * from "./utils/processingSteps.js";
27
27
  export * from "./utils/streamUrls.js";
28
+ export * from "./utils/embeddedMedia.js";
29
+ export * from "./types/embeddedMedia.js";
30
+ export * from "./composables/useMLMediaRenewal.js";
28
31
  export * from "./utils/webAppDeploys.js";
29
32
  export * from "./composables/useMLWebAppDeploys.js";
30
33
  export * from "./composables/useMLMediaMeta.js";
@@ -1,6 +1,7 @@
1
1
  import { defineNuxtPlugin } from "#app";
2
2
  import { watch } from "vue";
3
3
  import { useMLAssetPreview } from "../composables/useMLAssetPreview.js";
4
+ import { warnDeprecated } from "../utils/deprecation.js";
4
5
  function applyAssetToElement(el, asset, arg) {
5
6
  if (!asset || !asset.url) return;
6
7
  if (arg === "bg") {
@@ -33,6 +34,7 @@ function setupWatcher(el, id, arg) {
33
34
  }
34
35
  const assetDirective = {
35
36
  mounted(el, binding) {
37
+ warnDeprecated("v-asset");
36
38
  if (import.meta.server) return;
37
39
  const id = binding.value || "";
38
40
  const arg = binding.arg;
@@ -0,0 +1,25 @@
1
+ export interface MLEmbeddedMediaSource {
2
+ url: string;
3
+ width: number;
4
+ height?: number | null;
5
+ }
6
+ export interface MLEmbeddedMedia {
7
+ kind?: string | null;
8
+ /** The file; null when the viewer may not open it or it is not ready. */
9
+ url?: string | null;
10
+ /** The one image to show; set even for a viewer who may not open the file. */
11
+ poster?: string | null;
12
+ /** The poster's sizes, smallest first. */
13
+ sources?: readonly MLEmbeddedMediaSource[] | null;
14
+ streams?: {
15
+ hls?: string | null;
16
+ dash?: string | null;
17
+ } | null;
18
+ /** Seconds until the first carried link expires; null: none expires. */
19
+ expiresIn?: number | null;
20
+ }
21
+ /** What a player plays: the HLS ladder (adaptive), else the file. */
22
+ export interface MLPlayableSource {
23
+ src: string;
24
+ type: 'hls' | 'progressive';
25
+ }
File without changes
@@ -0,0 +1,2 @@
1
+ export declare const EMBEDDED_MEDIA_HINT: string;
2
+ export declare function warnDeprecated(name: string, hint?: string): void;
@@ -0,0 +1,7 @@
1
+ const warned = /* @__PURE__ */ new Set();
2
+ export const EMBEDDED_MEDIA_HINT = "Show the media the owning service embeds in its response instead (`media.poster`, `media.sources`, `media.streams.hls`, `media.url`); the browser should not resolve asset ids. See nuxt-media-library docs/design/embedded-media-migration-guide.md.";
3
+ export function warnDeprecated(name, hint = EMBEDDED_MEDIA_HINT) {
4
+ if (!import.meta.dev || warned.has(name)) return;
5
+ warned.add(name);
6
+ console.warn(`[nuxt-media-library] ${name} is deprecated and goes with the media library's /delivery. ${hint}`);
7
+ }
@@ -0,0 +1,20 @@
1
+ import type { MLEmbeddedMedia, MLPlayableSource } from '../types/embeddedMedia.js';
2
+ /** The image to show, or `fallback` when the media has none. */
3
+ export declare function mlMediaPoster<F extends string | null | undefined = undefined>(media: MLEmbeddedMedia | null | undefined, fallback?: F): string | F;
4
+ /**
5
+ * `srcset` for the poster `<img>`: its sizes from `media.sources`, so the
6
+ * browser loads the size the box needs (pair it with a `sizes` that is an
7
+ * upper bound of the rendered width). Undefined when there are none: `src`
8
+ * is then the only size.
9
+ */
10
+ export declare function mlMediaSrcset(media: MLEmbeddedMedia | null | undefined): string | undefined;
11
+ /**
12
+ * What a player should play: the HLS ladder, else the file. `preferFile`
13
+ * skips the ladder (a player falling back after the stream failed). Null when
14
+ * the viewer may not play it or it is not ready.
15
+ */
16
+ export declare function mlPlayableSource(media: MLEmbeddedMedia | null | undefined, { preferFile }?: {
17
+ preferFile?: boolean;
18
+ }): MLPlayableSource | null;
19
+ /** A video the viewer can play now (a stream or the file). */
20
+ export declare function mlIsPlayableVideo(media: MLEmbeddedMedia | null | undefined): boolean;
@@ -0,0 +1,15 @@
1
+ export function mlMediaPoster(media, fallback) {
2
+ return media?.poster || fallback;
3
+ }
4
+ export function mlMediaSrcset(media) {
5
+ const sources = media?.sources;
6
+ return sources?.length ? sources.map((s) => `${s.url} ${s.width}w`).join(", ") : void 0;
7
+ }
8
+ export function mlPlayableSource(media, { preferFile = false } = {}) {
9
+ if (!media) return null;
10
+ if (!preferFile && media.streams?.hls) return { src: media.streams.hls, type: "hls" };
11
+ return media.url ? { src: media.url, type: "progressive" } : null;
12
+ }
13
+ export function mlIsPlayableVideo(media) {
14
+ return media?.kind === "video" && !!mlPlayableSource(media);
15
+ }
@@ -5,5 +5,12 @@ import type { StreamUrlFields } from '../types/asset.js';
5
5
  * Null when the asset has no stream URLs or no lifetime (nothing to refresh).
6
6
  */
7
7
  export declare function streamUrlsRefreshDelay(asset: StreamUrlFields | null | undefined, resolvedAt: number, now?: number, marginSeconds?: number): number | null;
8
+ /**
9
+ * Milliseconds from `now` until links that lasted `ttlSeconds` from
10
+ * `resolvedAt` (ms epoch) should be fetched again, `marginSeconds` early
11
+ * (capped at half the lifetime); 0 when already due. Null when there is no
12
+ * lifetime: the links never expire.
13
+ */
14
+ export declare function refreshDelay(ttlSeconds: number | null | undefined, resolvedAt: number, now?: number, marginSeconds?: number): number | null;
8
15
  /** True when the asset's stream URLs are expired or about to. */
9
16
  export declare function streamUrlsStale(asset: StreamUrlFields | null | undefined, resolvedAt: number, now?: number, marginSeconds?: number): boolean;
@@ -1,10 +1,12 @@
1
1
  const DEFAULT_MARGIN_SECONDS = 30;
2
2
  export function streamUrlsRefreshDelay(asset, resolvedAt, now = Date.now(), marginSeconds = DEFAULT_MARGIN_SECONDS) {
3
3
  if (!asset || !asset.hlsUrl && !asset.dashUrl) return null;
4
- const ttl = asset.streamUrlExpiresIn;
5
- if (ttl == null || !Number.isFinite(ttl) || ttl <= 0) return null;
6
- const margin = Math.min(marginSeconds, ttl / 2);
7
- return Math.max(0, resolvedAt + (ttl - margin) * 1e3 - now);
4
+ return refreshDelay(asset.streamUrlExpiresIn, resolvedAt, now, marginSeconds);
5
+ }
6
+ export function refreshDelay(ttlSeconds, resolvedAt, now = Date.now(), marginSeconds = DEFAULT_MARGIN_SECONDS) {
7
+ if (ttlSeconds == null || !Number.isFinite(ttlSeconds) || ttlSeconds <= 0) return null;
8
+ const margin = Math.min(marginSeconds, ttlSeconds / 2);
9
+ return Math.max(0, resolvedAt + (ttlSeconds - margin) * 1e3 - now);
8
10
  }
9
11
  export function streamUrlsStale(asset, resolvedAt, now = Date.now(), marginSeconds = DEFAULT_MARGIN_SECONDS) {
10
12
  return streamUrlsRefreshDelay(asset, resolvedAt, now, marginSeconds) === 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-media-library",
3
- "version": "0.0.61",
3
+ "version": "0.0.62",
4
4
  "description": "Nuxt module for media library management — chunked uploads, asset previews, and resource policies",
5
5
  "repository": {
6
6
  "type": "git",