From d336a52d9feb8414f3e910c2f458e3580d9243cd Mon Sep 17 00:00:00 2001 From: DrMint <29893320+DrMint@users.noreply.github.com> Date: Sat, 22 Jun 2024 20:30:58 +0200 Subject: [PATCH] Added files collection --- src/collections/Collectibles/Collectibles.ts | 63 ++++++--- .../endpoints/getBySlugEndpoint.ts | 8 ++ src/collections/Files/Files.ts | 71 ++++++++++ src/collections/Files/endpoints/getByID.ts | 96 +++++++++++++ src/collections/Folders/Folders.ts | 1 + .../Folders/endpoints/getBySlugEndpoint.ts | 7 + src/collections/Recorders/Recorders.ts | 1 + src/constants.ts | 1 + src/endpoints/getAllPathsEndpoint.ts | 8 ++ src/payload.config.ts | 15 +- src/sdk.ts | 19 +++ src/types/collections.ts | 132 ++++++++++++------ src/utils/asserts.ts | 5 +- 13 files changed, 359 insertions(+), 68 deletions(-) create mode 100644 src/collections/Files/Files.ts create mode 100644 src/collections/Files/endpoints/getByID.ts diff --git a/src/collections/Collectibles/Collectibles.ts b/src/collections/Collectibles/Collectibles.ts index e9c2ecc..b6a873c 100644 --- a/src/collections/Collectibles/Collectibles.ts +++ b/src/collections/Collectibles/Collectibles.ts @@ -113,6 +113,8 @@ const fields = { contents: "contents", contentsContent: "content", + files: "files", + pageInfo: "pageInfo", pageInfoBindingType: "bindingType", pageInfoPageCount: "pageCount", @@ -229,7 +231,7 @@ export const Collectibles = buildVersionedCollectionConfig({ type: "array", admin: { description: - "Additional images of the item (unboxing, on shelf, promotional images...)", + "Additional images of the collectible (e.g: unboxing, on shelf, promotional images...)", }, labels: { singular: "Image", plural: "Images" }, fields: [ @@ -444,7 +446,7 @@ export const Collectibles = buildVersionedCollectionConfig({ label: "URLs", type: "array", admin: { - description: "Links to official websites where to get/buy the item.", + description: "Links to official websites where to get/buy the collectible.", }, fields: [{ name: fields.urlsUrl, type: "text", required: true }], }, @@ -567,33 +569,27 @@ export const Collectibles = buildVersionedCollectionConfig({ label: "Contents", fields: [ rowField([ - backPropagationField({ - name: fields.folders, - relationTo: Collections.Folders, - hasMany: true, - where: ({ id }) => ({ - and: [ - { "files.value": { equals: id } }, - { "files.relationTo": { equals: Collections.Collectibles } }, - ] as Where[], - }), - admin: { - description: `You can go to the "Folders" collection to include this collectible in a folder.`, - }, - }), - backPropagationField({ - name: fields.parentItems, - relationTo: Collections.Collectibles, - hasMany: true, - where: ({ id }) => ({ [fields.subitems]: { equals: id } }), - }), { name: fields.subitems, type: "relationship", hasMany: true, relationTo: Collections.Collectibles, + admin: { + description: + "Collectibles that are part of this collectible (e.g: artbook in a collector's edition, booklet in a CD...)", + }, + }, + { + name: fields.files, + type: "relationship", + hasMany: true, + relationTo: Collections.Files, + admin: { + description: "Files related to the collectible (e.g: zip of all the scans)", + }, }, ]), + { name: fields.contents, type: "array", @@ -699,6 +695,29 @@ export const Collectibles = buildVersionedCollectionConfig({ }, ], }, + + rowField([ + backPropagationField({ + name: fields.folders, + relationTo: Collections.Folders, + hasMany: true, + where: ({ id }) => ({ + and: [ + { "files.value": { equals: id } }, + { "files.relationTo": { equals: Collections.Collectibles } }, + ] as Where[], + }), + admin: { + description: `You can go to the "Folders" collection to include this collectible in a folder.`, + }, + }), + backPropagationField({ + name: fields.parentItems, + relationTo: Collections.Collectibles, + hasMany: true, + where: ({ id }) => ({ [fields.subitems]: { equals: id } }), + }), + ]), ], }, ], diff --git a/src/collections/Collectibles/endpoints/getBySlugEndpoint.ts b/src/collections/Collectibles/endpoints/getBySlugEndpoint.ts index bb3f6f2..cff7a53 100644 --- a/src/collections/Collectibles/endpoints/getBySlugEndpoint.ts +++ b/src/collections/Collectibles/endpoints/getBySlugEndpoint.ts @@ -5,6 +5,7 @@ import { Collectible } from "../../../types/collections"; import { isAudio, isDefined, + isFile, isImage, isNotEmpty, isPayloadArrayType, @@ -21,6 +22,7 @@ import { getDomainFromUrl, } from "../../../utils/endpoints"; import { convertAudioToEndpointAudioPreview } from "../../Audios/endpoints/getByID"; +import { convertFileToEndpointFilePreview } from "../../Files/endpoints/getByID"; import { convertPageToEndpointPagePreview } from "../../Pages/endpoints/getBySlugEndpoint"; import { convertRecorderToEndpointRecorderPreview } from "../../Recorders/endpoints/getByID"; import { convertVideoToEndpointVideoPreview } from "../../Videos/endpoints/getByID"; @@ -64,6 +66,7 @@ const convertCollectibleToEndpointCollectible = (collectible: Collectible): Endp nature, urls, subitems, + files, gallery: rawGallery, contents, priceEnabled, @@ -110,6 +113,11 @@ const convertCollectibleToEndpointCollectible = (collectible: Collectible): Endp subitems: isPayloadArrayType(subitems) ? subitems.filter(isPublished).map(convertCollectibleToEndpointCollectiblePreview) : [], + files: + files?.flatMap((file) => { + if (!isPayloadType(file) || !isFile(file)) return []; + return convertFileToEndpointFilePreview(file); + }) ?? [], contents: handleContents(contents), ...handlePrice(price, priceEnabled), createdAt, diff --git a/src/collections/Files/Files.ts b/src/collections/Files/Files.ts new file mode 100644 index 0000000..9c76d3d --- /dev/null +++ b/src/collections/Files/Files.ts @@ -0,0 +1,71 @@ +import { CollectionGroups, Collections } from "../../constants"; +import { attributesField } from "../../fields/attributesField/attributesField"; +import { creditsField } from "../../fields/creditsField/creditsField"; +import { imageField } from "../../fields/imageField/imageField"; +import { rowField } from "../../fields/rowField/rowField"; +import { translatedFields } from "../../fields/translatedFields/translatedFields"; +import { buildCollectionConfig } from "../../utils/collectionConfig"; +import { createEditor } from "../../utils/editor"; +import { getByID } from "./endpoints/getByID"; + +const fields = { + filename: "filename", + mimeType: "mimeType", + filesize: "filesize", + updatedAt: "updatedAt", + translations: "translations", + translationsPretitle: "pretitle", + translationsTitle: "title", + translationsSubtitle: "subtitle", + translationsDescription: "description", + thumbnail: "thumbnail", + attributes: "attributes", + credits: "credits", +}; + +export const Files = buildCollectionConfig({ + slug: Collections.Files, + labels: { singular: "File", plural: "Files" }, + defaultSort: fields.filename, + admin: { + group: CollectionGroups.Media, + preview: ({ id }) => `${process.env.PAYLOAD_PUBLIC_FRONTEND_BASE_URL}/en/files/${id}`, + description: "For any file that isn't a video, an image, or an audio file.", + defaultColumns: [ + fields.filename, + fields.thumbnail, + fields.mimeType, + fields.filesize, + fields.translations, + fields.updatedAt, + ], + }, + upload: { + disableLocalStorage: true, + }, + endpoints: [getByID], + fields: [ + imageField({ + name: fields.thumbnail, + relationTo: Collections.MediaThumbnails, + }), + translatedFields({ + name: fields.translations, + admin: { useAsTitle: fields.translationsTitle }, + fields: [ + rowField([ + { name: fields.translationsPretitle, type: "text" }, + { name: fields.translationsTitle, type: "text", required: true }, + { name: fields.translationsSubtitle, type: "text" }, + ]), + { + name: fields.translationsDescription, + type: "richText", + editor: createEditor({ inlines: true, lists: true, links: true }), + }, + ], + }), + attributesField({ name: fields.attributes }), + creditsField({ name: fields.credits }), + ], +}); diff --git a/src/collections/Files/endpoints/getByID.ts b/src/collections/Files/endpoints/getByID.ts new file mode 100644 index 0000000..17b8dfb --- /dev/null +++ b/src/collections/Files/endpoints/getByID.ts @@ -0,0 +1,96 @@ +import payload from "payload"; +import { Collections } from "../../../constants"; +import { EndpointFile, EndpointFilePreview, PayloadMedia } from "../../../sdk"; +import { File } from "../../../types/collections"; +import { CollectionEndpoint } from "../../../types/payload"; +import { isFile, isMediaThumbnail, isNotEmpty } from "../../../utils/asserts"; +import { + convertAttributesToEndpointAttributes, + convertCreditsToEndpointCredits, + convertMediaThumbnailToEndpointPayloadImage, + convertRTCToEndpointRTC, + getLanguageId, +} from "../../../utils/endpoints"; + +export const getByID: CollectionEndpoint = { + method: "get", + path: "/id/:id", + handler: async (req, res) => { + if (!req.user) { + return res.status(403).send({ + errors: [ + { + message: "You are not allowed to perform this action.", + }, + ], + }); + } + + if (!req.params.id) { + return res.status(400).send({ errors: [{ message: "Missing 'id' query params" }] }); + } + + try { + const result = await payload.findByID({ + collection: Collections.Files, + id: req.params.id, + }); + + if (!isFile(result)) { + return res.sendStatus(404); + } + + return res.status(200).json(convertFileToEndpointFile(result)); + } catch { + return res.sendStatus(404); + } + }, +}; + +export const convertFileToEndpointFilePreview = ({ + url, + attributes, + translations, + mimeType, + filename, + id, + thumbnail, + filesize, +}: File & PayloadMedia): EndpointFilePreview => ({ + id, + url, + filename, + filesize, + mimeType, + attributes: convertAttributesToEndpointAttributes(attributes), + translations: + translations?.map(({ language, title, pretitle, subtitle }) => ({ + language: getLanguageId(language), + ...(isNotEmpty(pretitle) ? { pretitle } : {}), + title, + ...(isNotEmpty(subtitle) ? { subtitle } : {}), + })) ?? [], + ...(isMediaThumbnail(thumbnail) + ? { thumbnail: convertMediaThumbnailToEndpointPayloadImage(thumbnail) } + : {}), +}); + +const convertFileToEndpointFile = (file: File & PayloadMedia): EndpointFile => { + const { translations, createdAt, updatedAt, filesize, credits } = file; + + return { + ...convertFileToEndpointFilePreview(file), + createdAt, + filesize, + updatedAt, + translations: + translations?.map(({ language, title, pretitle, subtitle, description }) => ({ + language: getLanguageId(language), + ...(isNotEmpty(pretitle) ? { pretitle } : {}), + title, + ...(isNotEmpty(subtitle) ? { subtitle } : {}), + ...(isNotEmpty(description) ? { description: convertRTCToEndpointRTC(description) } : {}), + })) ?? [], + credits: convertCreditsToEndpointCredits(credits), + }; +}; diff --git a/src/collections/Folders/Folders.ts b/src/collections/Folders/Folders.ts index 60eee6b..b4bcefe 100644 --- a/src/collections/Folders/Folders.ts +++ b/src/collections/Folders/Folders.ts @@ -107,6 +107,7 @@ export const Folders = buildCollectionConfig({ Collections.Videos, Collections.Images, Collections.Audios, + Collections.Files, ], hasMany: true, }, diff --git a/src/collections/Folders/endpoints/getBySlugEndpoint.ts b/src/collections/Folders/endpoints/getBySlugEndpoint.ts index f4e754f..fe470d0 100644 --- a/src/collections/Folders/endpoints/getBySlugEndpoint.ts +++ b/src/collections/Folders/endpoints/getBySlugEndpoint.ts @@ -5,6 +5,7 @@ import { Folder, Language } from "../../../types/collections"; import { isAudio, isDefined, + isFile, isImage, isNotEmpty, isPayloadType, @@ -14,6 +15,7 @@ import { import { convertSourceToEndpointSource, getLanguageId } from "../../../utils/endpoints"; import { convertAudioToEndpointAudioPreview } from "../../Audios/endpoints/getByID"; import { convertCollectibleToEndpointCollectiblePreview } from "../../Collectibles/endpoints/getBySlugEndpoint"; +import { convertFileToEndpointFilePreview } from "../../Files/endpoints/getByID"; import { convertImageToEndpointImagePreview } from "../../Images/endpoints/getByID"; import { convertPageToEndpointPagePreview } from "../../Pages/endpoints/getBySlugEndpoint"; import { convertVideoToEndpointVideoPreview } from "../../Videos/endpoints/getByID"; @@ -105,6 +107,11 @@ const convertFolderToEndpointFolder = (folder: Folder): EndpointFolder => { return [ { relationTo: Collections.Videos, value: convertVideoToEndpointVideoPreview(value) }, ]; + case Collections.Files: + if (!isFile(value)) return []; + return [ + { relationTo: Collections.Files, value: convertFileToEndpointFilePreview(value) }, + ]; default: return []; } diff --git a/src/collections/Recorders/Recorders.ts b/src/collections/Recorders/Recorders.ts index 4649f5f..2909bde 100644 --- a/src/collections/Recorders/Recorders.ts +++ b/src/collections/Recorders/Recorders.ts @@ -31,6 +31,7 @@ export const Recorders = buildCollectionConfig({ defaultSort: fields.username, admin: { useAsTitle: fields.username, + preview: ({ id }) => `${process.env.PAYLOAD_PUBLIC_FRONTEND_BASE_URL}/en/recorders/${id}`, description: "Recorders are contributors of the Accord's Library project. Ask an admin to create a \ Recorder here to be able to credit them in other collections.", diff --git a/src/constants.ts b/src/constants.ts index 6036036..d221367 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -10,6 +10,7 @@ export enum Collections { Collectibles = "collectibles", CreditsRole = "credits-roles", Currencies = "currencies", + Files = "files", Folders = "folders", GenericContents = "generic-contents", Images = "images", diff --git a/src/endpoints/getAllPathsEndpoint.ts b/src/endpoints/getAllPathsEndpoint.ts index 03c7818..07ab58f 100644 --- a/src/endpoints/getAllPathsEndpoint.ts +++ b/src/endpoints/getAllPathsEndpoint.ts @@ -69,6 +69,13 @@ export const getAllPathsEndpoint: Endpoint = { user: req.user, }); + const files = await payload.find({ + collection: Collections.Files, + depth: 0, + pagination: false, + user: req.user, + }); + const recorders = await payload.find({ collection: Collections.Recorders, depth: 0, @@ -95,6 +102,7 @@ export const getAllPathsEndpoint: Endpoint = { videos: videos.docs.map(({ id }) => id), audios: audios.docs.map(({ id }) => id), images: images.docs.map(({ id }) => id), + files: files.docs.map(({ id }) => id), recorders: recorders.docs.map(({ id }) => id), chronologyEvents: chronologyEvents.docs.map(({ id }) => id), }; diff --git a/src/payload.config.ts b/src/payload.config.ts index b1ed049..76978e4 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -10,6 +10,7 @@ import { ChronologyEvents } from "./collections/ChronologyEvents/ChronologyEvent import { Collectibles } from "./collections/Collectibles/Collectibles"; import { CreditsRoles } from "./collections/CreditsRoles/CreditsRoles"; import { Currencies } from "./collections/Currencies/Currencies"; +import { Files } from "./collections/Files/Files"; import { Folders } from "./collections/Folders/Folders"; import { GenericContents } from "./collections/GenericContents/GenericContents"; import { Images } from "./collections/Images/Images"; @@ -30,7 +31,7 @@ import { Collections } from "./constants"; import { getAllPathsEndpoint } from "./endpoints/getAllPathsEndpoint"; import { createEditor } from "./utils/editor"; -const configuredFtpAdapter = sftpAdapter({ +const configuredSftpAdapter = sftpAdapter({ connectOptions: { host: process.env.SFTP_HOST, username: process.env.SFTP_USERNAME, @@ -66,6 +67,7 @@ export default buildConfig({ Videos, VideosSubtitles, VideosChannels, + Files, Scans, Tags, @@ -96,17 +98,22 @@ export default buildConfig({ cloudStorage({ collections: { [Collections.Videos]: { - adapter: configuredFtpAdapter, + adapter: configuredSftpAdapter, disableLocalStorage: true, disablePayloadAccessControl: true, }, [Collections.VideosSubtitles]: { - adapter: configuredFtpAdapter, + adapter: configuredSftpAdapter, disableLocalStorage: true, disablePayloadAccessControl: true, }, [Collections.Audios]: { - adapter: configuredFtpAdapter, + adapter: configuredSftpAdapter, + disableLocalStorage: true, + disablePayloadAccessControl: true, + }, + [Collections.Files]: { + adapter: configuredSftpAdapter, disableLocalStorage: true, disablePayloadAccessControl: true, }, diff --git a/src/sdk.ts b/src/sdk.ts index b088617..e4291cc 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -54,6 +54,10 @@ export type EndpointFolder = EndpointFolderPreview & { relationTo: Collections.Videos; value: EndpointVideoPreview; } + | { + relationTo: Collections.Files; + value: EndpointFilePreview; + } )[]; parentPages: EndpointSource[]; }; @@ -223,6 +227,7 @@ export type EndpointCollectible = EndpointCollectiblePreview & { pageOrder?: CollectiblePageOrders; }; subitems: EndpointCollectiblePreview[]; + files: EndpointFilePreview[]; contents: { content: | { @@ -498,6 +503,16 @@ export type EndpointVideo = EndpointMedia & { duration: number; }; +export type EndpointFilePreview = EndpointMediaPreview & { + filesize: number; + thumbnail?: EndpointPayloadImage; +}; + +export type EndpointFile = EndpointMedia & { + filesize: number; + thumbnail?: EndpointPayloadImage; +}; + export type EndpointPayloadImage = PayloadImage & { sizes: PayloadImage[]; openGraph?: PayloadImage; @@ -523,6 +538,7 @@ export type EndpointAllPaths = { videos: string[]; audios: string[]; images: string[]; + files: string[]; recorders: string[]; chronologyEvents: string[]; }; @@ -565,6 +581,7 @@ export const getSDKEndpoint = { getImageByIDEndpoint: (id: string) => `/${Collections.Images}/id/${id}`, getAudioByIDEndpoint: (id: string) => `/${Collections.Audios}/id/${id}`, getVideoByIDEndpoint: (id: string) => `/${Collections.Videos}/id/${id}`, + getFileByIDEndpoint: (id: string) => `/${Collections.Files}/id/${id}`, getRecorderByIDEndpoint: (id: string) => `/${Collections.Recorders}/id/${id}`, getAllPathsEndpoint: () => `/all-paths`, getLoginEndpoint: () => `/${Collections.Recorders}/login`, @@ -659,6 +676,8 @@ export const getPayloadSDK = ({ await request(getSDKEndpoint.getAudioByIDEndpoint(id)), getVideoByID: async (id: string): Promise => await request(getSDKEndpoint.getVideoByIDEndpoint(id)), + getFileByID: async (id: string): Promise => + await request(getSDKEndpoint.getFileByIDEndpoint(id)), getRecorderByID: async (id: string): Promise => await request(getSDKEndpoint.getRecorderByIDEndpoint(id)), getAllPaths: async (): Promise => diff --git a/src/types/collections.ts b/src/types/collections.ts index b6f9f1d..034ff83 100644 --- a/src/types/collections.ts +++ b/src/types/collections.ts @@ -30,6 +30,7 @@ export interface Config { videos: Video; "videos-subtitles": VideoSubtitle; "videos-channels": VideosChannel; + files: File; scans: Scan; tags: Tag; attributes: Attribute; @@ -414,6 +415,10 @@ export interface Folder { relationTo: "audios"; value: string | Audio; } + | { + relationTo: "files"; + value: string | File; + } )[] | null; updatedAt: string; @@ -536,9 +541,8 @@ export interface Collectible { bindingType?: ("Paperback" | "Hardcover") | null; pageOrder?: ("Left to right" | "Right to left") | null; }; - folders?: (string | Folder)[] | null; - parentItems?: (string | Collectible)[] | null; subitems?: (string | Collectible)[] | null; + files?: (string | File)[] | null; contents?: | { content: @@ -603,6 +607,8 @@ export interface Collectible { id?: string | null; }[] | null; + folders?: (string | Folder)[] | null; + parentItems?: (string | Collectible)[] | null; updatedBy: string | Recorder; updatedAt: string; createdAt: string; @@ -676,49 +682,35 @@ export interface Currency { } /** * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "generic-contents". + * via the `definition` "files". */ -export interface GenericContent { +export interface File { id: string; - name: string; - translations: { - language: string | Language; - name: string; - id?: string | null; - }[]; - updatedAt: string; - createdAt: string; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "audios". - */ -export interface Audio { - id: string; - duration: number; thumbnail?: string | MediaThumbnail | null; - translations: { - language: string | Language; - pretitle?: string | null; - title: string; - subtitle?: string | null; - description?: { - root: { - type: string; - children: { - type: string; - version: number; + translations?: + | { + language: string | Language; + pretitle?: string | null; + title: string; + subtitle?: string | null; + description?: { + root: { + type: string; + children: { + type: string; + version: number; + [k: string]: unknown; + }[]; + direction: ("ltr" | "rtl") | null; + format: "left" | "start" | "center" | "right" | "end" | "justify" | ""; + indent: number; + version: number; + }; [k: string]: unknown; - }[]; - direction: ("ltr" | "rtl") | null; - format: "left" | "start" | "center" | "right" | "end" | "justify" | ""; - indent: number; - version: number; - }; - [k: string]: unknown; - } | null; - id?: string | null; - }[]; + } | null; + id?: string | null; + }[] + | null; attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null; credits?: Credits; updatedAt: string; @@ -823,6 +815,64 @@ export interface MediaThumbnail { }; }; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "generic-contents". + */ +export interface GenericContent { + id: string; + name: string; + translations: { + language: string | Language; + name: string; + id?: string | null; + }[]; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "audios". + */ +export interface Audio { + id: string; + duration: number; + thumbnail?: string | MediaThumbnail | null; + translations: { + language: string | Language; + pretitle?: string | null; + title: string; + subtitle?: string | null; + description?: { + root: { + type: string; + children: { + type: string; + version: number; + [k: string]: unknown; + }[]; + direction: ("ltr" | "rtl") | null; + format: "left" | "start" | "center" | "right" | "end" | "justify" | ""; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; + id?: string | null; + }[]; + attributes?: (TagsBlock | NumberBlock | TextBlock)[] | null; + credits?: Credits; + updatedAt: string; + createdAt: string; + url?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "videos". diff --git a/src/utils/asserts.ts b/src/utils/asserts.ts index 0bbc247..7e5c564 100644 --- a/src/utils/asserts.ts +++ b/src/utils/asserts.ts @@ -1,6 +1,6 @@ import { RichTextContent, isNodeParagraphNode } from "../constants"; import { PayloadImage, PayloadMedia } from "../sdk"; -import { Audio, Image, MediaThumbnail, Scan, Video } from "../types/collections"; +import { Audio, File, Image, MediaThumbnail, Scan, Video } from "../types/collections"; export const isDefined = (value: T | null | undefined): value is T => value !== null && value !== undefined; @@ -55,6 +55,9 @@ export const isPayloadImage = (image: unknown): image is PayloadImage => { export const isVideo = (video: string | Video | null | undefined): video is PayloadMedia & Video => isPayloadMedia(video); +export const isFile = (file: string | File | null | undefined): file is PayloadMedia & File => + isPayloadMedia(file); + export const isAudio = (video: string | Audio | null | undefined): video is PayloadMedia & Audio => isPayloadMedia(video);