Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New process to upload a broadcast replay to IGTV #1364

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions examples/live-replay-to-igtv.example.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
9 changes: 9 additions & 0 deletions src/errors/ig-upload-live-igtv-error.ts
Original file line number Diff line number Diff line change
@@ -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<UploadRepositoryVideoResponseRootObject>) {
super(response);
}
}
1 change: 1 addition & 0 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
31 changes: 0 additions & 31 deletions src/repositories/live.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
LiveSwitchCommentsResponseRootObject,
LiveCreateBroadcastResponseRootObject,
LiveStartBroadcastResponseRootObject,
LiveAddPostLiveToIgtvResponseRootObject,
LiveCommentsResponseRootObject,
LiveHeartbeatViewerCountResponseRootObject,
LiveInfoResponseRootObject,
Expand Down Expand Up @@ -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<LiveAddPostLiveToIgtvResponseRootObject> {
const { body } = await this.client.request.send<LiveAddPostLiveToIgtvResponseRootObject>({
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/`,
Expand Down
2 changes: 2 additions & 0 deletions src/repositories/media.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -615,6 +616,7 @@ export class MediaRepository extends Repository {
_uuid: this.client.state.uuid,
}),
});

return body;
}

Expand Down
4 changes: 4 additions & 0 deletions src/repositories/upload.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
1 change: 0 additions & 1 deletion src/responses/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 0 additions & 5 deletions src/responses/live.add-post-live-to-igtv.response.ts

This file was deleted.

52 changes: 51 additions & 1 deletion src/services/publish.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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<UploadRepositoryVideoResponseRootObject>);
}
};
}

/**
* Gets duration in ms, width and height info for a video in the mp4 container
* @param buffer Buffer, containing the video-file
Expand Down Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion src/types/media.configure-to-igtv.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?:
Expand Down
8 changes: 8 additions & 0 deletions src/types/posting.live-igtv.options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface PostingLiveIgtvOptions {
file: Buffer;
title: string;
caption?: string;
broadcastId: string;
igtv_share_preview_to_feed?: '1' | '0';
transcodeDelay?: number
}
1 change: 1 addition & 0 deletions src/types/upload.photo.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export interface UploadPhotoOptions {
file: Buffer;
isSidecar?: boolean;
waterfallId?: string;
broadcastId?: string;
}