@financial-times/cp-content-pipeline-schema 3.17.0 → 3.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/CHANGELOG.md +14 -0
- package/lib/generated/index.d.ts +23 -2
- package/lib/helpers/qualityWorkaround.d.ts +2 -0
- package/lib/helpers/qualityWorkaround.js +37 -0
- package/lib/helpers/qualityWorkaround.js.map +1 -0
- package/lib/model/Clip.d.ts +1 -17
- package/lib/model/Clip.js +27 -8
- package/lib/model/Clip.js.map +1 -1
- package/lib/model/Clip.test.js +77 -0
- package/lib/model/Clip.test.js.map +1 -1
- package/lib/model/Topper.d.ts +1 -0
- package/lib/model/Topper.js +6 -1
- package/lib/model/Topper.js.map +1 -1
- package/lib/model/schemas/capi/article.d.ts +18 -0
- package/lib/model/schemas/capi/audio.d.ts +14 -0
- package/lib/model/schemas/capi/base-schema.d.ts +42 -0
- package/lib/model/schemas/capi/base-schema.js +2 -0
- package/lib/model/schemas/capi/base-schema.js.map +1 -1
- package/lib/model/schemas/capi/content-package.d.ts +14 -0
- package/lib/model/schemas/capi/custom-code-component.d.ts +18 -0
- package/lib/model/schemas/capi/index.d.ts +96 -0
- package/lib/model/schemas/capi/live-blog-package.d.ts +18 -0
- package/lib/model/schemas/capi/placeholder.d.ts +18 -0
- package/lib/model/schemas/capi/video.d.ts +14 -0
- package/lib/resolvers/clip.d.ts +4 -9
- package/lib/resolvers/clip.js +3 -0
- package/lib/resolvers/clip.js.map +1 -1
- package/lib/resolvers/index.d.ts +6 -9
- package/lib/resolvers/topper.d.ts +2 -0
- package/lib/resolvers/topper.js +4 -2
- package/lib/resolvers/topper.js.map +1 -1
- package/lib/types/clip.d.ts +21 -0
- package/lib/types/clip.js +3 -0
- package/lib/types/clip.js.map +1 -0
- package/package.json +1 -1
- package/queries/article.graphql +6 -0
- package/src/generated/index.ts +23 -2
- package/src/helpers/qualityWorkaround.ts +44 -0
- package/src/model/Clip.test.ts +95 -0
- package/src/model/Clip.ts +33 -27
- package/src/model/Topper.ts +7 -1
- package/src/model/schemas/capi/base-schema.ts +2 -0
- package/src/resolvers/clip.ts +3 -0
- package/src/resolvers/topper.ts +4 -2
- package/src/types/clip.ts +23 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/typedefs/clip.graphql +9 -0
- package/typedefs/topper.graphql +8 -2
package/lib/resolvers/clip.js
CHANGED
|
@@ -17,6 +17,9 @@ const resolvers = {
|
|
|
17
17
|
pixelWidth: (parent) => parent.pixelWidth ?? null,
|
|
18
18
|
pixelHeight: (parent) => parent.pixelHeight ?? null,
|
|
19
19
|
videoCodec: (parent) => parent.videoCodec ?? null,
|
|
20
|
+
quality: (parent) => parent.quality ?? null,
|
|
21
|
+
dppx: (parent) => parent.dppx ?? null,
|
|
22
|
+
previousSourceWidth: (parent) => parent.previousSourceWidth ?? null,
|
|
20
23
|
},
|
|
21
24
|
};
|
|
22
25
|
exports.default = resolvers;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clip.js","sourceRoot":"","sources":["../../src/resolvers/clip.ts"],"names":[],"mappings":";;AAEA,MAAM,SAAS,GAAG;IAChB,IAAI,EAAE;QACJ,aAAa,EAAE,GAAG,EAAE,CAAC,MAAM;QAC3B,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE;QACvC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE;QAC3B,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;QAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;QAC/B,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE;KACxB;IAED,UAAU,EAAE;QACV,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI;QACjD,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS;QACvC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI;QAC7C,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS;QACvC,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI;QACjD,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,IAAI,IAAI;QACnD,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"clip.js","sourceRoot":"","sources":["../../src/resolvers/clip.ts"],"names":[],"mappings":";;AAEA,MAAM,SAAS,GAAG;IAChB,IAAI,EAAE;QACJ,aAAa,EAAE,GAAG,EAAE,CAAC,MAAM;QAC3B,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE;QACvC,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE;QAC3B,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;QAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE;QAC/B,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE;KACxB;IAED,UAAU,EAAE;QACV,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI;QACjD,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS;QACvC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,IAAI,IAAI;QAC7C,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS;QACvC,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI;QACjD,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,IAAI,IAAI;QACnD,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,IAAI,IAAI;QACjD,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI;QAC3C,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI;QACrC,mBAAmB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,mBAAmB,IAAI,IAAI;KACpE;CACiE,CAAA;AAEpE,kBAAe,SAAS,CAAA"}
|
package/lib/resolvers/index.d.ts
CHANGED
|
@@ -61,6 +61,7 @@ declare const resolvers: {
|
|
|
61
61
|
isLargeHeadline: (topper: import("../model/Topper").Topper) => boolean;
|
|
62
62
|
layout: (topper: import("../model/Topper").Topper) => string;
|
|
63
63
|
columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
|
|
64
|
+
columnists: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept[] | null;
|
|
64
65
|
__resolveType?: import("../generated").TypeResolveFn<"DeepPortraitTopper" | "DeepLandscapeTopper" | "SplitTextTopper" | "FullBleedTopper" | "PodcastTopper" | "OpinionTopper" | "BrandedTopper" | "BasicTopper" | "TopperWithFlourish" | "PartnerContentTopper", import("../model/Topper").Topper, import("..").QueryContext> | undefined;
|
|
65
66
|
backgroundBox: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<boolean>>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
|
|
66
67
|
backgroundColour: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<"paper" | "wheat" | "white" | "black" | "claret" | "oxford" | "slate" | "crimson" | "sky" | "matisse">>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
|
|
@@ -76,6 +77,7 @@ declare const resolvers: {
|
|
|
76
77
|
isLargeHeadline: (topper: import("../model/Topper").Topper) => boolean;
|
|
77
78
|
layout: (topper: import("../model/Topper").Topper) => string;
|
|
78
79
|
columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
|
|
80
|
+
columnists: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept[] | null;
|
|
79
81
|
__resolveType?: import("../generated").TypeResolveFn<"DeepPortraitTopper" | "DeepLandscapeTopper" | "SplitTextTopper" | "FullBleedTopper" | "PodcastTopper" | "OpinionTopper" | "BrandedTopper" | "BasicTopper" | "TopperWithFlourish" | "PartnerContentTopper", import("../model/Topper").Topper, import("..").QueryContext> | undefined;
|
|
80
82
|
backgroundBox: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<boolean>>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
|
|
81
83
|
backgroundColour: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<"paper" | "wheat" | "white" | "black" | "claret" | "oxford" | "slate" | "crimson" | "sky" | "matisse">>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
|
|
@@ -342,15 +344,7 @@ declare const resolvers: {
|
|
|
342
344
|
MetaLink: import("../generated").MetaLinkResolvers;
|
|
343
345
|
Clip: {
|
|
344
346
|
__resolveType: () => string;
|
|
345
|
-
dataSource: (clip: import("../model/Clip").Clip) =>
|
|
346
|
-
binaryUrl: string;
|
|
347
|
-
mediaType: string;
|
|
348
|
-
audioCodec?: string;
|
|
349
|
-
duration?: number;
|
|
350
|
-
pixelHeight?: number;
|
|
351
|
-
pixelWidth?: number;
|
|
352
|
-
videoCodec?: string;
|
|
353
|
-
}[];
|
|
347
|
+
dataSource: (clip: import("../model/Clip").Clip) => import("../types/clip").ClipSource[];
|
|
354
348
|
type: (clip: import("../model/Clip").Clip) => string;
|
|
355
349
|
format: (clip: import("../model/Clip").Clip) => "mobile" | "standard-inline";
|
|
356
350
|
poster: (clip: import("../model/Clip").Clip) => string;
|
|
@@ -364,6 +358,9 @@ declare const resolvers: {
|
|
|
364
358
|
pixelWidth: (parent: import("../generated").ClipSource) => number | null;
|
|
365
359
|
pixelHeight: (parent: import("../generated").ClipSource) => number | null;
|
|
366
360
|
videoCodec: (parent: import("../generated").ClipSource) => string | null;
|
|
361
|
+
quality: (parent: import("../generated").ClipSource) => string | null;
|
|
362
|
+
dppx: (parent: import("../generated").ClipSource) => number | null;
|
|
363
|
+
previousSourceWidth: (parent: import("../generated").ClipSource) => number | null;
|
|
367
364
|
};
|
|
368
365
|
Image: {
|
|
369
366
|
__resolveType: import("../generated").TypeResolveFn<"ImageDesktop" | "ImageLandscape" | "ImageMobile" | "ImagePortrait" | "ImageSquare" | "ImageSquareFTEdit" | "ImageStandard" | "ImageStandardInline" | "ImageWide", import("../model/Image").Image, import("..").QueryContext>;
|
|
@@ -31,6 +31,7 @@ declare const resolvers: {
|
|
|
31
31
|
isLargeHeadline: (topper: import("../model/Topper").Topper) => boolean;
|
|
32
32
|
layout: (topper: import("../model/Topper").Topper) => string;
|
|
33
33
|
columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
|
|
34
|
+
columnists: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept[] | null;
|
|
34
35
|
__resolveType?: import("../generated").TypeResolveFn<"DeepPortraitTopper" | "DeepLandscapeTopper" | "SplitTextTopper" | "FullBleedTopper" | "PodcastTopper" | "OpinionTopper" | "BrandedTopper" | "BasicTopper" | "TopperWithFlourish" | "PartnerContentTopper", import("../model/Topper").Topper, import("..").QueryContext> | undefined;
|
|
35
36
|
backgroundBox: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<boolean>>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
|
|
36
37
|
backgroundColour: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<"paper" | "wheat" | "white" | "black" | "claret" | "oxford" | "slate" | "crimson" | "sky" | "matisse">>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
|
|
@@ -46,6 +47,7 @@ declare const resolvers: {
|
|
|
46
47
|
isLargeHeadline: (topper: import("../model/Topper").Topper) => boolean;
|
|
47
48
|
layout: (topper: import("../model/Topper").Topper) => string;
|
|
48
49
|
columnist: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept | null;
|
|
50
|
+
columnists: (topper: import("../model/Topper").Topper) => import("../model/Concept").Concept[] | null;
|
|
49
51
|
__resolveType?: import("../generated").TypeResolveFn<"DeepPortraitTopper" | "DeepLandscapeTopper" | "SplitTextTopper" | "FullBleedTopper" | "PodcastTopper" | "OpinionTopper" | "BrandedTopper" | "BasicTopper" | "TopperWithFlourish" | "PartnerContentTopper", import("../model/Topper").Topper, import("..").QueryContext> | undefined;
|
|
50
52
|
backgroundBox: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<boolean>>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
|
|
51
53
|
backgroundColour: import("../generated").Resolver<import("../generated").Maybe<import("../generated").ResolverTypeWrapper<"paper" | "wheat" | "white" | "black" | "claret" | "oxford" | "slate" | "crimson" | "sky" | "matisse">>, import("../model/Topper").Topper, import("..").QueryContext, {}>;
|
package/lib/resolvers/topper.js
CHANGED
|
@@ -36,14 +36,16 @@ const resolvers = {
|
|
|
36
36
|
headshot: (topper, args) => topper.headshot(args),
|
|
37
37
|
isLargeHeadline: (topper) => topper.isLargeHeadline(),
|
|
38
38
|
layout: (topper) => topper.layout(),
|
|
39
|
-
columnist: (topper) => topper.columnist(),
|
|
39
|
+
columnist: (topper) => topper.columnist(), // @deprecated Replaced with usage of `columinists`
|
|
40
|
+
columnists: (topper) => topper.columnists(),
|
|
40
41
|
},
|
|
41
42
|
OpinionTopper: {
|
|
42
43
|
...topperResolvers,
|
|
43
44
|
headshot: (topper, args) => topper.headshot(args),
|
|
44
45
|
isLargeHeadline: (topper) => topper.isLargeHeadline(),
|
|
45
46
|
layout: (topper) => topper.layout(),
|
|
46
|
-
columnist: (topper) => topper.columnist(),
|
|
47
|
+
columnist: (topper) => topper.columnist(), // @deprecated Replaced with usage of `columinists`
|
|
48
|
+
columnists: (topper) => topper.columnists(),
|
|
47
49
|
},
|
|
48
50
|
TopperWithPackage: {
|
|
49
51
|
design: (topper) => topper.design().theme,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topper.js","sourceRoot":"","sources":["../../src/resolvers/topper.ts"],"names":[],"mappings":";;AAmBA,MAAM,eAAe,GAAoB;IACvC,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE;IACjD,gBAAgB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,gBAAgB,EAAE;IACvD,cAAc,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,EAAE;IACnD,mBAAmB,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,mBAAmB,EAAE;IAC7D,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;IAC/C,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE;IACvC,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;IACjC,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE;CAC5C,CAAA;AAED,MAAM,SAAS,GAAG;IAChB,MAAM,EAAE;QACN,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE;QACxC,GAAG,eAAe;KACnB;IAED,gBAAgB,EAAE;QAChB,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;QACnC,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE;KAClD;IAED,eAAe,EAAE;QACf,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;KACpC;IAED,eAAe,EAAE;QACf,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;QAC/C,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;KAChD;IAED,kBAAkB,EAAE;QAClB,QAAQ,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;KAClD;IAED,aAAa,EAAE;QACb,GAAG,eAAe;QAClB,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,EAAE;QAC/C,QAAQ,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QACjD,eAAe,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;QACnC,SAAS,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE;
|
|
1
|
+
{"version":3,"file":"topper.js","sourceRoot":"","sources":["../../src/resolvers/topper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LiteralUnionScalarValues } from '../resolvers/literal-union';
|
|
2
|
+
import { ClipFormat } from '../resolvers/scalars';
|
|
3
|
+
export type ClipSource = {
|
|
4
|
+
binaryUrl: string;
|
|
5
|
+
mediaType: string;
|
|
6
|
+
audioCodec?: string;
|
|
7
|
+
duration?: number;
|
|
8
|
+
pixelHeight?: number;
|
|
9
|
+
pixelWidth?: number;
|
|
10
|
+
videoCodec?: string;
|
|
11
|
+
quality?: string;
|
|
12
|
+
dppx?: number;
|
|
13
|
+
previousSourceWidth?: number;
|
|
14
|
+
};
|
|
15
|
+
export interface ClipVideo {
|
|
16
|
+
id(): string;
|
|
17
|
+
type(): string;
|
|
18
|
+
format(): LiteralUnionScalarValues<typeof ClipFormat>;
|
|
19
|
+
poster(): string;
|
|
20
|
+
dataSource(): ClipSource[];
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clip.js","sourceRoot":"","sources":["../../src/types/clip.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
package/queries/article.graphql
CHANGED
|
@@ -199,6 +199,9 @@ fragment Topper on Topper {
|
|
|
199
199
|
columnist {
|
|
200
200
|
...Concept
|
|
201
201
|
}
|
|
202
|
+
columnists {
|
|
203
|
+
...Concept
|
|
204
|
+
}
|
|
202
205
|
}
|
|
203
206
|
... on PodcastTopper {
|
|
204
207
|
headshot(dpr: 2, width: 160)
|
|
@@ -355,6 +358,9 @@ fragment Clip on Clip {
|
|
|
355
358
|
pixelHeight
|
|
356
359
|
pixelWidth
|
|
357
360
|
videoCodec
|
|
361
|
+
quality
|
|
362
|
+
dppx
|
|
363
|
+
previousSourceWidth
|
|
358
364
|
}
|
|
359
365
|
poster
|
|
360
366
|
}
|
package/src/generated/index.ts
CHANGED
|
@@ -377,6 +377,8 @@ export type ClipSource = {
|
|
|
377
377
|
readonly audioCodec?: Maybe<Scalars['String']['output']>;
|
|
378
378
|
/** The url of the clip source, eg. 'https://next-media-api.ft.com/renditions/16868569859480/0x0.mp3'. */
|
|
379
379
|
readonly binaryUrl: Scalars['String']['output'];
|
|
380
|
+
/** The encoding settings intention for the video media */
|
|
381
|
+
readonly dppx?: Maybe<Scalars['Int']['output']>;
|
|
380
382
|
/** The duration of the clip in milliseconds. */
|
|
381
383
|
readonly duration?: Maybe<Scalars['Int']['output']>;
|
|
382
384
|
/** The media type eg. video/mp4. */
|
|
@@ -385,6 +387,10 @@ export type ClipSource = {
|
|
|
385
387
|
readonly pixelHeight?: Maybe<Scalars['Int']['output']>;
|
|
386
388
|
/** The width of the clip in pixels. */
|
|
387
389
|
readonly pixelWidth?: Maybe<Scalars['Int']['output']>;
|
|
390
|
+
/** The width of the source immediately narrower than this one. */
|
|
391
|
+
readonly previousSourceWidth?: Maybe<Scalars['Int']['output']>;
|
|
392
|
+
/** The encoding quality of the video media */
|
|
393
|
+
readonly quality?: Maybe<Scalars['String']['output']>;
|
|
388
394
|
/** The video encoding format of the source, eg. h264. */
|
|
389
395
|
readonly videoCodec?: Maybe<Scalars['String']['output']>;
|
|
390
396
|
};
|
|
@@ -1318,8 +1324,13 @@ export type OpinionTopper = Topper & TopperWithHeadshot & TopperWithTheme & {
|
|
|
1318
1324
|
readonly backgroundBox?: Maybe<Scalars['Boolean']['output']>;
|
|
1319
1325
|
/** The background colour of the topper. */
|
|
1320
1326
|
readonly backgroundColour?: Maybe<Scalars['TopperBackgroundColour']['output']>;
|
|
1321
|
-
/**
|
|
1327
|
+
/**
|
|
1328
|
+
* The concept object describing details of the column author.
|
|
1329
|
+
* @deprecated use columnists instead
|
|
1330
|
+
*/
|
|
1322
1331
|
readonly columnist?: Maybe<Concept>;
|
|
1332
|
+
/** The concept object describing details of the column author(s). */
|
|
1333
|
+
readonly columnists?: Maybe<ReadonlyArray<Concept>>;
|
|
1323
1334
|
/** The concept object to be displayed, eg. {'type': 'TOPIC', 'prefLabel': 'Non-dom tax status', ...}. */
|
|
1324
1335
|
readonly displayConcept?: Maybe<Concept>;
|
|
1325
1336
|
/** The variant of the follow button to be displayed on the topper. */
|
|
@@ -1549,8 +1560,13 @@ export type PodcastTopper = Topper & TopperWithBrand & TopperWithHeadshot & Topp
|
|
|
1549
1560
|
readonly backgroundColour?: Maybe<Scalars['TopperBackgroundColour']['output']>;
|
|
1550
1561
|
/** The concept object of the brand, eg. {'type': 'BRAND', 'prefLabel': 'FT Magazine', ...}. */
|
|
1551
1562
|
readonly brandConcept?: Maybe<Concept>;
|
|
1552
|
-
/**
|
|
1563
|
+
/**
|
|
1564
|
+
* The concept object describing details of the column author.
|
|
1565
|
+
* @deprecated use columnists instead
|
|
1566
|
+
*/
|
|
1553
1567
|
readonly columnist?: Maybe<Concept>;
|
|
1568
|
+
/** The concept object describing details of the column author(s). */
|
|
1569
|
+
readonly columnists?: Maybe<ReadonlyArray<Concept>>;
|
|
1554
1570
|
/** The concept object to be displayed, eg. {'type': 'TOPIC', 'prefLabel': 'Non-dom tax status', ...}. */
|
|
1555
1571
|
readonly displayConcept?: Maybe<Concept>;
|
|
1556
1572
|
/** The variant of the follow button to be displayed on the topper. */
|
|
@@ -2403,10 +2419,13 @@ export type ClipSetResolvers<ContextType = QueryContext, ParentType extends Reso
|
|
|
2403
2419
|
export type ClipSourceResolvers<ContextType = QueryContext, ParentType extends ResolversParentTypes['ClipSource'] = ResolversParentTypes['ClipSource']> = ResolversObject<{
|
|
2404
2420
|
audioCodec: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
|
2405
2421
|
binaryUrl: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
2422
|
+
dppx: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
|
|
2406
2423
|
duration: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
|
|
2407
2424
|
mediaType: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
|
2408
2425
|
pixelHeight: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
|
|
2409
2426
|
pixelWidth: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
|
|
2427
|
+
previousSourceWidth: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
|
|
2428
|
+
quality: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
|
2410
2429
|
videoCodec: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
|
2411
2430
|
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
|
2412
2431
|
}>;
|
|
@@ -2913,6 +2932,7 @@ export type OpinionTopperResolvers<ContextType = QueryContext, ParentType extend
|
|
|
2913
2932
|
backgroundBox: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
|
|
2914
2933
|
backgroundColour: Resolver<Maybe<ResolversTypes['TopperBackgroundColour']>, ParentType, ContextType>;
|
|
2915
2934
|
columnist: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
2935
|
+
columnists: Resolver<Maybe<ReadonlyArray<ResolversTypes['Concept']>>, ParentType, ContextType>;
|
|
2916
2936
|
displayConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
2917
2937
|
followButtonVariant: Resolver<Maybe<ResolversTypes['FollowButtonVariant']>, ParentType, ContextType>;
|
|
2918
2938
|
genreConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
@@ -3041,6 +3061,7 @@ export type PodcastTopperResolvers<ContextType = QueryContext, ParentType extend
|
|
|
3041
3061
|
backgroundColour: Resolver<Maybe<ResolversTypes['TopperBackgroundColour']>, ParentType, ContextType>;
|
|
3042
3062
|
brandConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
3043
3063
|
columnist: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
3064
|
+
columnists: Resolver<Maybe<ReadonlyArray<ResolversTypes['Concept']>>, ParentType, ContextType>;
|
|
3044
3065
|
displayConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
3045
3066
|
followButtonVariant: Resolver<Maybe<ResolversTypes['FollowButtonVariant']>, ParentType, ContextType>;
|
|
3046
3067
|
genreConcept: Resolver<Maybe<ResolversTypes['Concept']>, ParentType, ContextType>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// until we get quality setting data through from CAPI, we infer the quality and ddpx from the details in the filename
|
|
2
|
+
// once we have quality data from CAPI, we can remove this module
|
|
3
|
+
import type { ClipSource } from '../types/clip'
|
|
4
|
+
|
|
5
|
+
// possible output sizes are set in https://github.com/Financial-Times/ip-ovide/tree/main/common/n-zencoder/src/profiles
|
|
6
|
+
const dimensions = new Map([
|
|
7
|
+
[1920, 1080],
|
|
8
|
+
[1280, 720],
|
|
9
|
+
[960, 540],
|
|
10
|
+
[640, 360],
|
|
11
|
+
[480, 270],
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
// utility to extract width from filename pattern like /640x360.mp4
|
|
15
|
+
function getDimensionsFromFilename(url: string): number[] {
|
|
16
|
+
// regex: get the first set of digits after a "/" and before "x", followed by 1+ digits, followed by ".mp" and 1 digit, then end of string.
|
|
17
|
+
const widthRegex = /\/(\d+)x(\d+)\.mp\d$/i
|
|
18
|
+
const widthMatch = url.match(widthRegex)
|
|
19
|
+
return [
|
|
20
|
+
parseInt(widthMatch?.[1] ?? '0', 10),
|
|
21
|
+
parseInt(widthMatch?.[2] ?? '0', 10),
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function qualityWorkaround(
|
|
26
|
+
dataSource: ClipSource[]
|
|
27
|
+
): ClipSource[] {
|
|
28
|
+
return dataSource.map((source) => {
|
|
29
|
+
const originalDimensions = getDimensionsFromFilename(source.binaryUrl ?? '')
|
|
30
|
+
if (originalDimensions[0] === 0) {
|
|
31
|
+
// this will apply to audio files
|
|
32
|
+
return source
|
|
33
|
+
}
|
|
34
|
+
const smallerDimension = Math.min(...originalDimensions)
|
|
35
|
+
source.quality = `${
|
|
36
|
+
Array.from(dimensions.values()).includes(smallerDimension)
|
|
37
|
+
? smallerDimension
|
|
38
|
+
: dimensions.get(smallerDimension)
|
|
39
|
+
}p`
|
|
40
|
+
|
|
41
|
+
source.dppx = smallerDimension >= 1080 ? 2 : 1
|
|
42
|
+
return source
|
|
43
|
+
})
|
|
44
|
+
}
|
package/src/model/Clip.test.ts
CHANGED
|
@@ -4,6 +4,42 @@ const mockContext = {
|
|
|
4
4
|
systemCode: 'cp-content-pipeline',
|
|
5
5
|
} as unknown as QueryContext
|
|
6
6
|
|
|
7
|
+
const mockDataSources = [
|
|
8
|
+
{
|
|
9
|
+
audioCodec: 'mp3',
|
|
10
|
+
binaryUrl: 'https://clips.ft.com/someguid/0x0.mp3',
|
|
11
|
+
duration: 21720,
|
|
12
|
+
mediaType: 'audio/mpeg',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
audioCodec: 'aac',
|
|
16
|
+
binaryUrl: 'https://clips.ft.com/someguid/640x360.mp4',
|
|
17
|
+
duration: 21684,
|
|
18
|
+
mediaType: 'video/mp4',
|
|
19
|
+
pixelHeight: 360,
|
|
20
|
+
pixelWidth: 640,
|
|
21
|
+
videoCodec: 'h264',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
audioCodec: 'aac',
|
|
25
|
+
binaryUrl: 'https://clips.ft.com/someguid/1280x720.mp4',
|
|
26
|
+
duration: 21684,
|
|
27
|
+
mediaType: 'video/mp4',
|
|
28
|
+
pixelHeight: 720,
|
|
29
|
+
pixelWidth: 1280,
|
|
30
|
+
videoCodec: 'h264',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
audioCodec: 'aac',
|
|
34
|
+
binaryUrl: 'https://clips.ft.com/someguid/1920x1080.mp4',
|
|
35
|
+
duration: 21684,
|
|
36
|
+
mediaType: 'video/mp4',
|
|
37
|
+
pixelHeight: 720,
|
|
38
|
+
pixelWidth: 1280,
|
|
39
|
+
videoCodec: 'h264',
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
|
|
7
43
|
describe('Clip', () => {
|
|
8
44
|
describe('poster', () => {
|
|
9
45
|
it('uses the Origami Image Service when a url is provided', () => {
|
|
@@ -170,4 +206,63 @@ describe('Clip', () => {
|
|
|
170
206
|
expect(clip.poster()).toContain('width=1200')
|
|
171
207
|
})
|
|
172
208
|
})
|
|
209
|
+
describe('datasources', () => {
|
|
210
|
+
it('creates a quality value and dppx value based on the width in the URL when quality is not provided', () => {
|
|
211
|
+
const clip = new Clip(
|
|
212
|
+
{
|
|
213
|
+
id: 'http://api.ft.com/things/1234',
|
|
214
|
+
type: 'http://www.ft.com/ontology/content/Clip',
|
|
215
|
+
dataSource: mockDataSources,
|
|
216
|
+
},
|
|
217
|
+
mockContext
|
|
218
|
+
)
|
|
219
|
+
const dataSource = clip.dataSource()
|
|
220
|
+
expect(dataSource).toHaveLength(4)
|
|
221
|
+
expect(dataSource?.[0]?.quality).toBe('1080p')
|
|
222
|
+
expect(dataSource?.[0]?.dppx).toBe(2)
|
|
223
|
+
expect(dataSource?.[1]?.quality).toBe('720p')
|
|
224
|
+
expect(dataSource?.[2]?.quality).toBe('360p')
|
|
225
|
+
expect(dataSource?.[3]?.quality).toBeUndefined()
|
|
226
|
+
})
|
|
227
|
+
it('orders the data sources by width and then dppx', () => {
|
|
228
|
+
const clip = new Clip(
|
|
229
|
+
{
|
|
230
|
+
id: 'http://api.ft.com/things/1234',
|
|
231
|
+
type: 'http://www.ft.com/ontology/content/Clip',
|
|
232
|
+
dataSource: mockDataSources,
|
|
233
|
+
},
|
|
234
|
+
mockContext
|
|
235
|
+
)
|
|
236
|
+
const dataSource = clip.dataSource()
|
|
237
|
+
expect(dataSource).toHaveLength(4)
|
|
238
|
+
expect(dataSource?.[0]?.binaryUrl).toBe(
|
|
239
|
+
'https://clips.ft.com/someguid/1920x1080.mp4'
|
|
240
|
+
)
|
|
241
|
+
expect(dataSource?.[1]?.binaryUrl).toBe(
|
|
242
|
+
'https://clips.ft.com/someguid/1280x720.mp4'
|
|
243
|
+
)
|
|
244
|
+
expect(dataSource?.[2]?.binaryUrl).toBe(
|
|
245
|
+
'https://clips.ft.com/someguid/640x360.mp4'
|
|
246
|
+
)
|
|
247
|
+
expect(dataSource?.[3]?.binaryUrl).toBe(
|
|
248
|
+
'https://clips.ft.com/someguid/0x0.mp3'
|
|
249
|
+
)
|
|
250
|
+
})
|
|
251
|
+
it('sets the previousSourceWidth to the next most narrow source', () => {
|
|
252
|
+
const clip = new Clip(
|
|
253
|
+
{
|
|
254
|
+
id: 'http://api.ft.com/things/1234',
|
|
255
|
+
type: 'http://www.ft.com/ontology/content/Clip',
|
|
256
|
+
dataSource: mockDataSources,
|
|
257
|
+
},
|
|
258
|
+
mockContext
|
|
259
|
+
)
|
|
260
|
+
const dataSource = clip.dataSource()
|
|
261
|
+
expect(dataSource).toHaveLength(4)
|
|
262
|
+
expect(dataSource?.[0]?.previousSourceWidth).toBe(640)
|
|
263
|
+
expect(dataSource?.[1]?.previousSourceWidth).toBe(640)
|
|
264
|
+
expect(dataSource?.[2]?.previousSourceWidth).toBeUndefined()
|
|
265
|
+
expect(dataSource?.[3]?.previousSourceWidth).toBeUndefined()
|
|
266
|
+
})
|
|
267
|
+
})
|
|
173
268
|
})
|
package/src/model/Clip.ts
CHANGED
|
@@ -6,25 +6,9 @@ import {
|
|
|
6
6
|
} from '../resolvers/literal-union'
|
|
7
7
|
import { ClipFormat } from '../resolvers/scalars'
|
|
8
8
|
import imageServiceUrl from '../helpers/imageService'
|
|
9
|
+
import qualityWorkaround from '../helpers/qualityWorkaround'
|
|
9
10
|
import { QueryContext } from '..'
|
|
10
|
-
|
|
11
|
-
type ClipSource = {
|
|
12
|
-
binaryUrl: string
|
|
13
|
-
mediaType: string
|
|
14
|
-
audioCodec?: string
|
|
15
|
-
duration?: number
|
|
16
|
-
pixelHeight?: number
|
|
17
|
-
pixelWidth?: number
|
|
18
|
-
videoCodec?: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface ClipVideo {
|
|
22
|
-
id(): string
|
|
23
|
-
type(): string
|
|
24
|
-
format(): LiteralUnionScalarValues<typeof ClipFormat>
|
|
25
|
-
poster(): string
|
|
26
|
-
dataSource(): ClipSource[]
|
|
27
|
-
}
|
|
11
|
+
import type { ClipSource, ClipVideo } from '../types/clip'
|
|
28
12
|
|
|
29
13
|
export class Clip implements ClipVideo {
|
|
30
14
|
constructor(
|
|
@@ -60,18 +44,40 @@ export class Clip implements ClipVideo {
|
|
|
60
44
|
return 'standard-inline'
|
|
61
45
|
}
|
|
62
46
|
|
|
47
|
+
// apply 'quality' workaround
|
|
48
|
+
// order sources by width and then quality
|
|
49
|
+
// get the min width from the first previous more narrow item
|
|
63
50
|
dataSource() {
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
51
|
+
// create a copy of the array so we don't modify the original data
|
|
52
|
+
let dataSource = this.clip.dataSource.map((src) => ({
|
|
53
|
+
...src,
|
|
54
|
+
})) as ClipSource[]
|
|
55
|
+
dataSource = qualityWorkaround(dataSource as ClipSource[])
|
|
56
|
+
dataSource = dataSource.sort((a, b) => {
|
|
57
|
+
const widthDiff = (b.pixelWidth ?? 0) - (a.pixelWidth ?? 0)
|
|
58
|
+
if (widthDiff !== 0) {
|
|
59
|
+
return widthDiff
|
|
71
60
|
}
|
|
72
|
-
return 0
|
|
61
|
+
return (b?.dppx ?? 0) - (a?.dppx ?? 0)
|
|
73
62
|
})
|
|
74
|
-
|
|
63
|
+
|
|
64
|
+
// get the min width from the previous item if it has pixelWidth set, checking it is not the same as pixelWidth as the current item
|
|
65
|
+
for (let i = 0; i < dataSource.length; i++) {
|
|
66
|
+
let next = 1
|
|
67
|
+
let minWidth = dataSource[i + next]?.pixelWidth
|
|
68
|
+
while (
|
|
69
|
+
typeof minWidth !== 'undefined' &&
|
|
70
|
+
minWidth === dataSource[i]?.pixelWidth
|
|
71
|
+
) {
|
|
72
|
+
next++
|
|
73
|
+
minWidth = dataSource[i + next]?.pixelWidth
|
|
74
|
+
}
|
|
75
|
+
if (minWidth && dataSource[i]) {
|
|
76
|
+
dataSource[i]!.previousSourceWidth = minWidth
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return dataSource
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
poster() {
|
package/src/model/Topper.ts
CHANGED
|
@@ -289,14 +289,20 @@ export class Topper {
|
|
|
289
289
|
return 'standard'
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
// @deprecated Replaced with usage of `columinists`
|
|
292
293
|
columnist() {
|
|
294
|
+
const authors = this.columnists()
|
|
295
|
+
return authors?.[0] ?? null
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
columnists() {
|
|
293
299
|
const authors = this.capiResponse.annotations({
|
|
294
300
|
byPredicate: predicates.hasAuthor,
|
|
295
301
|
})
|
|
296
302
|
const isOpinionOrColumn =
|
|
297
303
|
this.type() === 'OpinionTopper' || this.capiResponse.isColumn()
|
|
298
304
|
|
|
299
|
-
return isOpinionOrColumn && authors
|
|
305
|
+
return isOpinionOrColumn && authors.length > 0 ? authors : null
|
|
300
306
|
}
|
|
301
307
|
|
|
302
308
|
brandConcept() {
|
|
@@ -103,6 +103,8 @@ const ClipSource = z.object({
|
|
|
103
103
|
pixelHeight: z.number().optional(),
|
|
104
104
|
pixelWidth: z.number().optional(),
|
|
105
105
|
videoCodec: z.string().optional(),
|
|
106
|
+
quality: z.string().optional(),
|
|
107
|
+
dppx: z.number().optional(),
|
|
106
108
|
})
|
|
107
109
|
|
|
108
110
|
export const Clip = z.object({
|
package/src/resolvers/clip.ts
CHANGED
|
@@ -18,6 +18,9 @@ const resolvers = {
|
|
|
18
18
|
pixelWidth: (parent) => parent.pixelWidth ?? null,
|
|
19
19
|
pixelHeight: (parent) => parent.pixelHeight ?? null,
|
|
20
20
|
videoCodec: (parent) => parent.videoCodec ?? null,
|
|
21
|
+
quality: (parent) => parent.quality ?? null,
|
|
22
|
+
dppx: (parent) => parent.dppx ?? null,
|
|
23
|
+
previousSourceWidth: (parent) => parent.previousSourceWidth ?? null,
|
|
21
24
|
},
|
|
22
25
|
} satisfies { Clip: ClipResolvers; ClipSource: ClipSourceResolvers }
|
|
23
26
|
|
package/src/resolvers/topper.ts
CHANGED
|
@@ -59,7 +59,8 @@ const resolvers = {
|
|
|
59
59
|
headshot: (topper, args) => topper.headshot(args),
|
|
60
60
|
isLargeHeadline: (topper) => topper.isLargeHeadline(),
|
|
61
61
|
layout: (topper) => topper.layout(),
|
|
62
|
-
columnist: (topper) => topper.columnist(),
|
|
62
|
+
columnist: (topper) => topper.columnist(), // @deprecated Replaced with usage of `columinists`
|
|
63
|
+
columnists: (topper) => topper.columnists(),
|
|
63
64
|
},
|
|
64
65
|
|
|
65
66
|
OpinionTopper: {
|
|
@@ -67,7 +68,8 @@ const resolvers = {
|
|
|
67
68
|
headshot: (topper, args) => topper.headshot(args),
|
|
68
69
|
isLargeHeadline: (topper) => topper.isLargeHeadline(),
|
|
69
70
|
layout: (topper) => topper.layout(),
|
|
70
|
-
columnist: (topper) => topper.columnist(),
|
|
71
|
+
columnist: (topper) => topper.columnist(), // @deprecated Replaced with usage of `columinists`
|
|
72
|
+
columnists: (topper) => topper.columnists(),
|
|
71
73
|
},
|
|
72
74
|
|
|
73
75
|
TopperWithPackage: {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { LiteralUnionScalarValues } from '../resolvers/literal-union'
|
|
2
|
+
import { ClipFormat } from '../resolvers/scalars'
|
|
3
|
+
|
|
4
|
+
export type ClipSource = {
|
|
5
|
+
binaryUrl: string
|
|
6
|
+
mediaType: string
|
|
7
|
+
audioCodec?: string
|
|
8
|
+
duration?: number
|
|
9
|
+
pixelHeight?: number
|
|
10
|
+
pixelWidth?: number
|
|
11
|
+
videoCodec?: string
|
|
12
|
+
quality?: string
|
|
13
|
+
dppx?: number
|
|
14
|
+
previousSourceWidth?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ClipVideo {
|
|
18
|
+
id(): string
|
|
19
|
+
type(): string
|
|
20
|
+
format(): LiteralUnionScalarValues<typeof ClipFormat>
|
|
21
|
+
poster(): string
|
|
22
|
+
dataSource(): ClipSource[]
|
|
23
|
+
}
|