diff --git a/examples/live-replay-to-igtv.example.ts b/examples/live-replay-to-igtv.example.ts new file mode 100644 index 000000000..12b558955 --- /dev/null +++ b/examples/live-replay-to-igtv.example.ts @@ -0,0 +1,104 @@ +/* tslint:disable:no-console */ +import {IgApiClient, LiveEntity} from '../src'; +import Bluebird = require('bluebird'); +const pngToJpeg = require('png-to-jpeg') +const sharp = require('sharp'); +const axios = require('axios'); + +const ig = new IgApiClient(); + +async function login() { + ig.state.generateDevice(process.env.IG_USERNAME); + ig.state.proxyUrl = process.env.IG_PROXY; + await ig.account.login(process.env.IG_USERNAME, process.env.IG_PASSWORD); +} + +(async () => { + // basic login-procedure + await login(); + + const {broadcast_id, upload_url} = await ig.live.create({ + // create a stream in 720x1280 (9:16) + previewWidth: 720, + previewHeight: 1280, + // this message is not necessary, because it doesn't show up in the notification + message: 'My message', + }); + // (optional) get the key and url for programs such as OBS + const {stream_key, stream_url} = LiveEntity.getUrlAndKey({broadcast_id, upload_url}); + console.log(`Start your stream on ${stream_url}.\n + Your key is: ${stream_key}`); + + /** + * make sure you are streaming to the url + * the next step will send a notification / start your stream for everyone to see + */ + const startInfo = await ig.live.start(broadcast_id); + // status should be 'ok' + console.log(startInfo); + + /** + * now, your stream is running + * the next step is to get comments + * note: comments can only be requested roughly every 2s + */ + + // initial comment-timestamp = 0, get all comments + let lastCommentTs = await printComments(broadcast_id, 0); + + // enable the comments + await ig.live.unmuteComment(broadcast_id); + /** + * wait 2 seconds until the next request. + * in the real world you'd use something like setInterval() instead of Bluebird.delay() / just to simulate a delay + */ + // wait 2s + await Bluebird.delay(2000); + // now, we print the next comments + lastCommentTs = await printComments(broadcast_id, lastCommentTs); + + // now we're commenting on our stream + await ig.live.comment(broadcast_id, 'A comment'); + + /** + * now, your stream is running, you entertain your followers, but you're tired and + * we're going to stop the stream + */ + await ig.live.endBroadcast(broadcast_id); + + // Get live thumbnails, required to post on IGTV + let data = await ig.live.getPostLiveThumbnails(broadcast_id); + + // Use an HTTP client to download any thumb + let {data: file} = await axios.get(data.thumbnails[0], {responseType: 'arraybuffer'}); + + // (optional) Resize thumb to a vertical one and convert to jpg + file = await sharp(file) + .resize({width: 720, height: 1280}) + .jpeg({ + quality: 100, + }) + .toBuffer(); + + // Upload the thumbnail with a broadcast id for a replay and get uploadId + let igtv = await ig.publish.liveIgtv({ + file, + broadcastId: broadcast_id, + title: 'A title', + caption: 'A description', + igtv_share_preview_to_feed: '1' + }); + + console.log(`Live posted to IGTV : ${igtv.upload_id}`); + // now you're basically done +})(); + +async function printComments(broadcastId, lastCommentTs) { + const {comments} = await ig.live.getComment({broadcastId, lastCommentTs}); + if (comments.length > 0) { + comments.forEach(comment => console.log(`${comment.user.username}: ${comment.text}`)); + return comments[comments.length - 1].created_at; + } else { + return lastCommentTs; + } +} diff --git a/src/errors/ig-upload-live-igtv-error.ts b/src/errors/ig-upload-live-igtv-error.ts new file mode 100644 index 000000000..8dbf2bec0 --- /dev/null +++ b/src/errors/ig-upload-live-igtv-error.ts @@ -0,0 +1,9 @@ +import { IgResponseError } from './ig-response.error'; +import { IgResponse } from '../types'; +import { UploadRepositoryVideoResponseRootObject } from '../responses'; + +export class IgUploadLiveIgtvError extends IgResponseError { + constructor(response: IgResponse) { + super(response); + } +} diff --git a/src/errors/index.ts b/src/errors/index.ts index bdf3fe24d..e41d3cf41 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -22,5 +22,6 @@ export * from './ig-challenge-wrong-code.error'; export * from './ig-exact-user-not-found-error'; export * from './ig-user-id-not-found.error'; export * from './ig-upload-video-error'; +export * from './ig-upload-live-igtv-error'; export * from './ig-user-has-logged-out.error'; export * from './ig-configure-video-error'; diff --git a/src/repositories/live.repository.ts b/src/repositories/live.repository.ts index c74fff5ea..bb15fcdad 100644 --- a/src/repositories/live.repository.ts +++ b/src/repositories/live.repository.ts @@ -4,7 +4,6 @@ import { LiveSwitchCommentsResponseRootObject, LiveCreateBroadcastResponseRootObject, LiveStartBroadcastResponseRootObject, - LiveAddPostLiveToIgtvResponseRootObject, LiveCommentsResponseRootObject, LiveHeartbeatViewerCountResponseRootObject, LiveInfoResponseRootObject, @@ -281,36 +280,6 @@ export class LiveRepository extends Repository { return body; } - public async addPostLiveToIgtv({ - broadcastId, - title, - description, - coverUploadId, - igtvSharePreviewToFeed = false, - }: { - broadcastId: string; - title: string; - description: string; - coverUploadId: string; - igtvSharePreviewToFeed?: boolean; - }): Promise { - const { body } = await this.client.request.send({ - url: `/api/v1/live/add_post_live_to_igtv/`, - method: 'POST', - form: this.client.request.sign({ - _csrftoken: this.client.state.cookieCsrfToken, - _uuid: this.client.state.uuid, - broadcast_id: broadcastId, - cover_upload_id: coverUploadId, - description: description, - title: title, - internal_only: false, - igtv_share_preview_to_feed: igtvSharePreviewToFeed, - }), - }); - return body; - } - public async endBroadcast(broadcastId: string, endAfterCopyrightWarning: boolean = false) { const { body } = await this.client.request.send({ url: `/api/v1/live/${broadcastId}/end_broadcast/`, diff --git a/src/repositories/media.repository.ts b/src/repositories/media.repository.ts index c66cd4d8f..40a2a21b6 100644 --- a/src/repositories/media.repository.ts +++ b/src/repositories/media.repository.ts @@ -598,6 +598,7 @@ export class MediaRepository extends Repository { }); const retryContext = options.retryContext; delete form.retryContext; + const { body } = await this.client.request.send({ url: '/api/v1/media/configure_to_igtv/', method: 'POST', @@ -615,6 +616,7 @@ export class MediaRepository extends Repository { _uuid: this.client.state.uuid, }), }); + return body; } diff --git a/src/repositories/upload.repository.ts b/src/repositories/upload.repository.ts index d0e7b1aeb..b35ee2513 100644 --- a/src/repositories/upload.repository.ts +++ b/src/repositories/upload.repository.ts @@ -208,6 +208,10 @@ export class UploadRepository extends Repository { if (options.isSidecar) { ruploadParams.is_sidecar = '1'; } + if (options.broadcastId) { + ruploadParams.broadcast_id = options.broadcastId; + ruploadParams.is_post_live_igtv = '1'; + } return ruploadParams; } diff --git a/src/responses/index.ts b/src/responses/index.ts index 1b43b076b..6b15b3809 100644 --- a/src/responses/index.ts +++ b/src/responses/index.ts @@ -37,7 +37,6 @@ export * from './live.like-count.response'; export * from './live.post-live-thumbnails.response'; export * from './live.like.response'; export * from './live.start-broadcast.response'; -export * from './live.add-post-live-to-igtv.response'; export * from './live.switch-comments.response'; export * from './live.viewer-list.response'; export * from './live.add-to-post.response'; diff --git a/src/responses/live.add-post-live-to-igtv.response.ts b/src/responses/live.add-post-live-to-igtv.response.ts deleted file mode 100644 index 3d91eafe4..000000000 --- a/src/responses/live.add-post-live-to-igtv.response.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface LiveAddPostLiveToIgtvResponseRootObject { - success: boolean; - igtv_post_id: number; - status: string; -} diff --git a/src/services/publish.service.ts b/src/services/publish.service.ts index 7b55e3ec7..9a4bda008 100644 --- a/src/services/publish.service.ts +++ b/src/services/publish.service.ts @@ -19,9 +19,10 @@ import { UploadVideoOptions, } from '../types'; import { PostingLocation, PostingStoryOptions } from '../types/posting.options'; -import { IgConfigureVideoError, IgResponseError, IgUploadVideoError } from '../errors'; +import { IgConfigureVideoError, IgResponseError, IgUploadLiveIgtvError, IgUploadVideoError } from '../errors'; import { StatusResponse, UploadRepositoryVideoResponseRootObject } from '../responses'; import { PostingIgtvOptions } from '../types/posting.igtv.options'; +import { PostingLiveIgtvOptions } from "../types/posting.live-igtv.options"; import sizeOf = require('image-size'); import Bluebird = require('bluebird'); import Chance = require('chance'); @@ -52,6 +53,22 @@ export class PublishService extends Repository { }; } + /** + * @param transcodeDelayInMs The delay for instagram to transcode the video + */ + public static catchLiveIgtvTranscodeError(transcodeDelayInMs: number) { + return error => { + if (error.response.statusCode === 202) { + PublishService.publishDebug( + `Received trancode error: ${JSON.stringify(error.response.body)}, waiting ${transcodeDelayInMs}ms`, + ); + return Bluebird.delay(transcodeDelayInMs); + } else { + throw new IgUploadLiveIgtvError(error.response as IgResponse); + } + }; + } + /** * Gets duration in ms, width and height info for a video in the mp4 container * @param buffer Buffer, containing the video-file @@ -453,6 +470,39 @@ export class PublishService extends Repository { } } + public async liveIgtv(options: PostingLiveIgtvOptions) { + const uploadedPhoto = await this.client.upload.photo({ + file: options.file, + broadcastId: options.broadcastId, + }); + + await Bluebird.try(() => + this.client.media.uploadFinish({ + upload_id: uploadedPhoto.upload_id, + source_type: '4', + }), + ).catch(IgResponseError, PublishService.catchLiveIgtvTranscodeError(options.transcodeDelay || 5000)); + + const configureOptions: MediaConfigureToIgtvOptions = { + upload_id: uploadedPhoto.upload_id, + title: options.title, + caption: options.caption, + igtv_share_preview_to_feed: options.igtv_share_preview_to_feed, + length: 0 + }; + + for (let i = 0; i < 6; i++) { + try { + return await this.client.media.configureToIgtv(configureOptions); + } catch (e) { + if (i >= 5 || e.response.statusCode >= 400) { + throw new IgConfigureVideoError(e.response, configureOptions); + } + await Bluebird.delay((i + 1) * 2 * 1000); + } + } + } + private async regularVideo(options: UploadVideoOptions) { options = defaults(options, { uploadId: Date.now(), diff --git a/src/types/media.configure-to-igtv.options.ts b/src/types/media.configure-to-igtv.options.ts index c2e83041b..aa28edd35 100644 --- a/src/types/media.configure-to-igtv.options.ts +++ b/src/types/media.configure-to-igtv.options.ts @@ -2,7 +2,7 @@ export interface MediaConfigureToIgtvOptions { upload_id: string; title: string; length: number; - extra: { source_width: number; source_height: number }; + extra?: { source_width: number; source_height: number }; caption?: string; // will be converted to a json-string feed_preview_crop?: diff --git a/src/types/posting.live-igtv.options.ts b/src/types/posting.live-igtv.options.ts new file mode 100644 index 000000000..073bcef23 --- /dev/null +++ b/src/types/posting.live-igtv.options.ts @@ -0,0 +1,8 @@ +export interface PostingLiveIgtvOptions { + file: Buffer; + title: string; + caption?: string; + broadcastId: string; + igtv_share_preview_to_feed?: '1' | '0'; + transcodeDelay?: number +} \ No newline at end of file diff --git a/src/types/upload.photo.options.ts b/src/types/upload.photo.options.ts index 45546962c..b522e899d 100644 --- a/src/types/upload.photo.options.ts +++ b/src/types/upload.photo.options.ts @@ -3,4 +3,5 @@ export interface UploadPhotoOptions { file: Buffer; isSidecar?: boolean; waterfallId?: string; + broadcastId?: string; }