linkedin-apilinkedin-jsapi

CORRUPTED_ENTITY video post linkedin api


We try to make video posts with the linkedin API, in the whole process of upload initialization, video segmentation, upload, upload finalization and post I have no errors, we even managed to publish the first time but now no more posts are published.

In the same way, when I try to publish a duplicate linkedin post, it refuses because it's a duplicate post.


CODE:

Upload of the video:

    async initializeUploadToLinkedinVideo(filePath) {
        const params = [
            { value: this.profileUrn, name: 'profileUrn' },
            { value: this.token, name: 'token' },
            { value: filePath, name: 'filePath' }
        ]
        const error = verifyParams(params)
        if (error) {
            return error
        }

        let fileSize
        try {
            fileSize = await getSize(filePath)
            if (fileSize === 0) {
                return { error: 'File size is 0' }
            }
        } catch (e) {
            console.error('Error getting file size:', e)
            return { error: 'Failed to get file size' }
        }

        try {
            const uploadResponse = await axios.post(
                `https://api.linkedin.com/rest/videos?action=initializeUpload`,
                {
                    initializeUploadRequest: {
                        owner: this.profileUrn,
                        fileSizeBytes: fileSize,
                        uploadCaptions: false,
                        uploadThumbnail: false
                    }
                },
                {
                    headers: {
                        'Authorization': `Bearer ${this.token}`,
                        'X-Restli-Protocol-Version': '2.0.0',
                        'LinkedIn-Version': '202405',
                        'Content-Type': 'application/json'
                    }
                }
            )
            return uploadResponse.data
        } catch (e) {
            console.error(`Error during LinkedIn initialize upload video:`, e.response ? e.response.data : e.message)
            return { error: 'Failed to upload to LinkedIn' }
        }
    }

    async uploadToLinkedinVideo(filePath, uploadInstructions) {
        const params = [
            { value: this.token, name: 'token'},
            { value: uploadInstructions, name: 'uploadInstructions' },
            { value: filePath, name: 'filePath' }
        ]
        const error = verifyParams(params)
        if (error) {
            return error
        }

        let videoSplit
        try {
            videoSplit = await videoSplitter(filePath, uploadInstructions)
            if (videoSplit?.error) {
                console.error('Error splitting video:', videoSplit.error)
                return videoSplit
            }
        } catch (e) {
            console.error('Error splitting video:', e)
            return { error: 'Failed to split video' }
        }

        console.log('videoSplit:', videoSplit)

        try {
            const uploadPromises = videoSplit.map(async (video) => {
                const { outputFilePath, uploadUrl } = video
                const fileStream = fs.createReadStream(outputFilePath)
                const formData = new FormData()
                formData.append('file', fileStream)

                const uploadResponse = await axios.put(uploadUrl, formData,
                    {
                        headers: {
                            ...formData.getHeaders()
                        }
                    }
                )
                return {
                    status: uploadResponse.status,
                    etag: uploadResponse.headers.etag
                }
            })
            return await Promise.all(uploadPromises)
        } catch (e) {
            console.error('Error during LinkedIn upload video:', e.response ? e.response.data : e.message)
            return { error: 'Failed to upload to LinkedIn' }
        }
    }

    async finalizeVideoUpload(videoUrn, eTags) {
        const params = [
            { value: this.token, name: 'token' },
            { value: videoUrn, name: 'videoUrn' }
        ]
        const error = verifyParams(params)
        if (error) {
            return error
        }

        try {
            const finalizeResponse = await axios.post(
                `https://api.linkedin.com/rest/videos?action=finalizeUpload`,
                {
                    finalizeUploadRequest: {
                        video: videoUrn,
                        uploadToken: "",
                        uploadedPartIds: eTags
                    }
                },
                {
                    headers: {
                        "Authorization": `Bearer ${this.token}`,
                        "X-Restli-Protocol-Version": "2.0.0",
                        "LinkedIn-Version": "202405",
                        "Content-Type": "application/json"
                    }
                }
            )
            return finalizeResponse.status
        } catch (e) {
            console.error('Error during LinkedIn finalize video upload:', e.response ? e.response.data : e.message)
            return { error: 'Failed to finalize video upload' }
        }
    }

    async initializeAndUploadToLinkedinVideo(filePath) {
        const initializeUploadToLinkedinResponse = await this.initializeUploadToLinkedinVideo(filePath)
        console.log('initializeUploadToLinkedinResponse:', initializeUploadToLinkedinResponse)
        if (initializeUploadToLinkedinResponse?.error) {
            return initializeUploadToLinkedinResponse
        }

        const videoUrn = initializeUploadToLinkedinResponse?.value?.video
        if (!videoUrn) {
            return { error: 'Failed to get video URN' }
        }

        const uploadInstructions = initializeUploadToLinkedinResponse?.value?.uploadInstructions
        console.log('uploadInstructions:', uploadInstructions)
        if (!uploadInstructions) {
            return { error: 'Failed to get upload instructions' }
        }

        const uploadToLinkedinResponse = await this.uploadToLinkedinVideo(filePath, uploadInstructions)
        console.log('uploadToLinkedinResponse:', uploadToLinkedinResponse)
        if (uploadToLinkedinResponse?.error) {
            return uploadToLinkedinResponse
        }

        const eTags = uploadToLinkedinResponse.map(upload => upload.etag)
        const finalizeVideoUploadResponse = await this.finalizeVideoUpload(videoUrn, eTags)
        console.log('finalizeVideoUploadResponse:', finalizeVideoUploadResponse)
        if (finalizeVideoUploadResponse?.error) {
            return finalizeVideoUploadResponse
        }
        if (finalizeVideoUploadResponse !== 200) {
            return { error: 'Failed to finalize video upload' }
        }

        return videoUrn
    }

NOTE: verifyParams(), getSize() and videoSplitter() are defined in an other file, below the function used to split the video according to the documentation

exports.videoSplitter = async (filePath, uploadInstructions) => {
    const outputDir = `./uploads/linkedin/${filePath.split('/').pop().split('.')[0]}`

    if (!fs.existsSync(outputDir)) {
        fs.mkdirSync(outputDir, { recursive: true })
    }

    const videoPaths = []

    const splitPromise = uploadInstructions.map((range, index) => {
        return new Promise((resolve, reject) => {
            const { firstByte, lastByte, uploadUrl } = range
            const outputFilePath = `${outputDir}/segment_${index + 1}.mp4`
            // Adjust the end position to include the last byte
            const readStream = fs.createReadStream(filePath, { start: firstByte, end: lastByte })
            const writeStream = fs.createWriteStream(outputFilePath)

            readStream.pipe(writeStream)

            readStream.on('end', () => {
                console.log(`Segment ${index + 1} written successfully`)
                const absolutePath = fs.realpathSync(outputFilePath)
                videoPaths.push({ outputFilePath: absolutePath, uploadUrl })
                resolve()
            })

            readStream.on('error', (err) => {
                console.error(`Error reading segment ${index + 1}:`, err)
                reject(err)
            })

            writeStream.on('error', (err) => {
                console.error(`Error writing segment ${index + 1}:`, err)
                reject(err)
            })
        })
    })

    try {
        await Promise.all(splitPromise)
    } catch (e) {
        console.error('Error splitting video:', e)
        return { error: 'Failed to split video', details: e }
    }

    if (videoPaths.length !== uploadInstructions.length) {
        return { error: 'Failed to split video' }
    }

    return videoPaths
}

Below the request to post and get a video:

Post:

POST https://api.linkedin.com/rest/posts
Authorization: Bearer <ACCESS_TOKEN>
Linkedin-Version: 202405
X-RestLi-Protocol-Version: 2.0.0
Content-Type: application/json

{
  "author": "urn:li:organization:<ORGANIZATION_ID>",
  "commentary": "Post video from api test longer /3!",
  "visibility": "PUBLIC",
  "distribution": {
    "feedDistribution": "MAIN_FEED",
    "targetEntities": [],
    "thirdPartyDistributionChannels": []
  },
  "content": {
    "media": {
      "title": "Post video from api test longer /2!",
      "id": "urn:li:video:<VIDEO_ID>"
    }
  },
  "lifecycleState": "PUBLISHED",
  "isReshareDisabledByAuthor": false
}

get:

GET https://api.linkedin.com/rest/videos/urn:li:video:<VIDEO_ID>
Authorization: Bearer <ACCESS_TOKEN>
Linkedin-Version: 202405
Content-Type: application/json

# Response:

{
  "owner": "urn:li:organization:<ORGANIZATION_ID>",
  "processingFailureReason": "CORRUPTED_ENTITY",
  "id": "urn:li:video:<VIDEO_ID>",
  "status": "PROCESSING_FAILED"
}

I also tried this but I have an issue when splitting the video.

I have the same issue when trying to upload video < 4Mb (unsplitted video)


Solution

  • The problem was in the way the videos were split in videoSplitter(), here is a corrected function

    const fs = require('fs')
    const path = require('path')
    const { exec } = require('child_process')
    
    exports.videoSplitter = async (filePath, uploadInstructions) => {
        const outputDir = path.join('./uploads/linkedin', path.basename(filePath, path.extname(filePath)))
    
        if (!fs.existsSync(outputDir)) {
            fs.mkdirSync(outputDir, { recursive: true })
        }
    
        const videoPaths = []
    
        const splitCommand = `split -b 4194303 ${filePath} ${path.join(outputDir, 'part-')}`
    
        try {
            await new Promise((resolve, reject) => {
                exec(splitCommand, (error, stdout, stderr) => {
                    if (error) {
                        console.error(`Error splitting video: ${error}`)
                        reject(error)
                    } else {
                        resolve()
                    }
                })
            })
    
            const partFiles = fs.readdirSync(outputDir).filter(file => file.startsWith('part-'))
    
            if (partFiles.length !== uploadInstructions.length) {
                return { error: 'Failed to split video: mismatch between number of parts and upload instructions' }
            }
    
            partFiles.forEach((file, index) => {
                const absolutePath = fs.realpathSync(path.join(outputDir, file))
                const uploadUrl = uploadInstructions[index].uploadUrl
                videoPaths.push({ outputFilePath: absolutePath, uploadUrl })
            })
    
        } catch (e) {
            console.error('Error splitting video:', e)
            return { error: 'Failed to split video', details: e }
        }
    
        return videoPaths
    }