diff --git a/packages/cli/package.json b/packages/cli/package.json index 2599fa1..0ff8670 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -34,9 +34,9 @@ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "devDependencies": { - "@metaplex-foundation/js": "0.20.0", "@jest/globals": "^29.5.0", "@jest/types": "^29.5.0", + "@metaplex-foundation/js": "0.20.0", "@swc/jest": "^0.2.26", "@types/commander": "^2.12.2", "@types/debug": "^4.1.7", @@ -64,6 +64,7 @@ "dotenv": "^16.0.3", "esm": "^3.2.25", "generate-schema": "^2.6.0", + "get-video-dimensions": "^1.0.0", "image-size": "^1.0.2", "js-yaml": "^4.1.0", "semver": "^7.3.8", diff --git a/packages/cli/src/config/PublishDetails.ts b/packages/cli/src/config/PublishDetails.ts index 8a87241..2be40aa 100644 --- a/packages/cli/src/config/PublishDetails.ts +++ b/packages/cli/src/config/PublishDetails.ts @@ -18,6 +18,7 @@ import { Constants, showMessage } from "../CliUtils.js"; import util from "util"; import { imageSize } from "image-size"; import { exec } from "child_process"; +import getVideoDimensions from "get-video-dimensions"; const runImgSize = util.promisify(imageSize); const runExec = util.promisify(exec); @@ -125,19 +126,45 @@ export const loadPublishDetailsWithChecks = async ( } config.release.media.forEach((item: PublishDetails["release"]["media"][0]) => { - const imagePath = path.join(process.cwd(), item.uri); - if (!fs.existsSync(imagePath) || !checkImageExtension(imagePath)) { - throw new Error(`Invalid media path or file type: ${item.uri}. Please ensure the file is a jpeg, png, or webp file.`); + const mediaPath = path.join(process.cwd(), item.uri); + if (!fs.existsSync(mediaPath)) { + throw new Error(`File doesnt exist: ${item.uri}.`) + } + + if (item.purpose == "screenshot" && !checkImageExtension(mediaPath)) { + throw new Error(`Please ensure the file ${item.uri} is a jpeg, png, or webp file.`) + } + + if (item.purpose == "video" && !checkVideoExtension(mediaPath)) { + throw new Error(`Please ensure the file ${item.uri} is a mp4.`) } } ); - const previewMediaFiles = config.release.media?.filter( - (asset: any) => asset.purpose === "screenshot" || asset.purpose === "video" + const screenshots = config.release.media?.filter( + (asset: any) => asset.purpose === "screenshot" + ) + + for (const item of screenshots) { + const mediaPath = path.join(process.cwd(), item.uri); + if (await checkScreenshotSize(mediaPath)) { + throw new Error(`Screenshot ${mediaPath} must be at least 1080px in width and height.`); + } + } + + const videos = config.release.media?.filter( + (asset: any) => asset.purpose === "video" ) - if (previewMediaFiles.length < 4) { - throw new Error(`At least 4 screenshots or videos are required for publishing a new release. Found only ${previewMediaFiles.length}`) + for (const video of videos) { + const mediaPath = path.join(process.cwd(), video.uri); + if (await checkVideoSize(mediaPath)) { + throw new Error(`Video ${mediaPath} must be at least 1080px in width and height.`); + } + } + + if (screenshots.length + videos.length < 4) { + throw new Error(`At least 4 screenshots or videos are required for publishing a new release. Found only ${screenshots.length + videos.length}`) } validateLocalizableResources(config); @@ -174,6 +201,13 @@ const checkImageExtension = (uri: string): boolean => { ); }; +const checkVideoExtension = (uri: string): boolean => { + const fileExt = path.extname(uri).toLowerCase(); + return ( + fileExt == ".mp4" + ); +}; + /** * We need to pre-check some things in the localized resources before we move forward */ @@ -206,6 +240,19 @@ const checkIconDimensions = async (iconPath: string): Promise => { return size?.width != size?.height || (size?.width ?? 0) != 512; }; +const checkScreenshotSize = async (imagePath: string): Promise => { + const size = await runImgSize(imagePath); + + return (size?.width ?? 0) < 1080 || (size?.height ?? 0) < 1080; +} + +const checkVideoSize = async (imagePath: string): Promise => { + const size = await getVideoDimensions(imagePath); + + return (size?.width ?? 0) < 1080 || (size?.height ?? 0) < 1080; +} + + const getAndroidDetails = async ( aaptDir: string, apkPath: string diff --git a/packages/core/package.json b/packages/core/package.json index ebcd26d..6e06abe 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -34,6 +34,7 @@ "@types/debug": "^4.1.7", "@types/mime": "^3.0.1", "@types/node-fetch": "^2.6.2", + "get-video-dimensions": "^1.0.0", "json-schema-to-typescript": "^11.0.2", "shx": "^0.3.4" }, diff --git a/packages/core/src/create/ReleaseCore.ts b/packages/core/src/create/ReleaseCore.ts index 1ec219c..d8db9f6 100644 --- a/packages/core/src/create/ReleaseCore.ts +++ b/packages/core/src/create/ReleaseCore.ts @@ -9,6 +9,7 @@ import { Constants, mintNft } from "../CoreUtils.js"; import * as util from "util"; import { metaplexFileReplacer, validateRelease } from "../validate/CoreValidation.js"; import { imageSize } from "image-size"; +import getVideoDimensions from "get-video-dimensions"; import type { Keypair, PublicKey } from "@solana/web3.js"; import type { @@ -46,14 +47,25 @@ const getFileMetadata = async (item: Media | File) => { }; const getMediaMetadata = async (item: Media) => { - const size = await runImgSize(item.uri ?? ""); const metadata = await getFileMetadata(item); - return { - ...metadata, - width: size?.width ?? 0, - height: size?.height ?? 0, - }; + if (item.purpose == "screenshot" || item.purpose == "icon") { + const size = await runImgSize(item.uri ?? ""); + + return { + ...metadata, + width: size?.width ?? 0, + height: size?.height ?? 0, + }; + } else { + const size = await getVideoDimensions(item.uri ?? ""); + + return { + ...metadata, + width: size?.width ?? 0, + height: size?.height ?? 0, + }; + } }; export const createReleaseJson = async ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aebc84..f4ee01a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: generate-schema: specifier: ^2.6.0 version: 2.6.0 + get-video-dimensions: + specifier: ^1.0.0 + version: 1.0.0 image-size: specifier: ^1.0.2 version: 1.0.2 @@ -196,6 +199,9 @@ importers: '@types/node-fetch': specifier: ^2.6.2 version: 2.6.2 + get-video-dimensions: + specifier: ^1.0.0 + version: 1.0.0 json-schema-to-typescript: specifier: ^11.0.2 version: 11.0.2 @@ -3358,7 +3364,6 @@ packages: /any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - dev: true /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} @@ -5119,6 +5124,11 @@ packages: get-intrinsic: 1.2.1 dev: true + /get-video-dimensions@1.0.0: + resolution: {integrity: sha512-ceq/GQySbquAdStqoejWWWCqBk8bDejcR6/0CbFUlXNAPBrqDMDKZVmGHU0PpzsK0SPYhX2jd/8tkAsbkUF4tw==} + dependencies: + mz: 1.3.0 + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -6517,6 +6527,13 @@ packages: /mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + /mz@1.3.0: + resolution: {integrity: sha512-x+R7YSsEySSpV5uEB+C47JTmxv+YKKNsW3W+hjvq8NbLn8ntLgYXGrR5RjQ3Fs0e7Chw8Rp/1e5eo0n5LP76cw==} + dependencies: + native-or-bluebird: 1.2.0 + thenify: 3.3.1 + thenify-all: 1.6.0 + /mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} dependencies: @@ -6525,6 +6542,10 @@ packages: thenify-all: 1.6.0 dev: true + /native-or-bluebird@1.2.0: + resolution: {integrity: sha512-0SH8UubxDfe382eYiwmd12qxAbiWGzlGZv6CkMA+DPojWa/Y0oH4hE0lRtFfFgJmPQFyKXeB8XxPbZz6TvvKaQ==} + deprecated: '''native-or-bluebird'' is deprecated. Please use ''any-promise'' instead.' + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -7567,13 +7588,11 @@ packages: engines: {node: '>=0.8'} dependencies: thenify: 3.3.1 - dev: true /thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} dependencies: any-promise: 1.3.0 - dev: true /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}