blob: df795b188ae5dcb29346cf5d62db61f0dffb248a [file] [log] [blame]
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright (c) 2022 Philipp Le <philipp@philipple.de>.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { IgApiClient, IgLoginTwoFactorRequiredError } from 'instagram-private-api';
import Bluebird = require("bluebird");
import inquirer = require ("inquirer");
import date = require("date-and-time");
import fs = require("fs");
import path = require("path");
import https = require("https");
export class MediaItem {
id: string = "";
taken_at: Date = new Date();
date_str: string = "";
url: string = "";
filename: string = "";
constructor(id: string, taken_at: number, url: string) {
this.id = id;
this.taken_at = new Date(taken_at * 1000);
this.date_str = date.format(this.taken_at, "YYYYMMDD");
this.url = url;
const filename = url.split('#').shift()?.split('?').shift()?.split('/').pop();
if (filename)
this.filename = filename;
else
this.filename = "";
}
public async fetch(dest_dir: string): Promise<void> {
const dest_path = path.join(dest_dir, this.filename);
console.log(`Downloading ${this.url} => ${dest_path}`);
const file = fs.createWriteStream(dest_path);
await new Promise((resolve) => {
https.get(this.url, (response) => {
response.pipe(file);
file.on("finish", () => {
file.close();
resolve(response);
});
})
});
console.log(`Completed downloading ${this.url} => ${dest_path}`);
}
}
export class UserStory extends MediaItem {
constructor(id: string, taken_at: number, url: string) {
super(id, taken_at, url);
}
}
export class StoryCollection {
users: Map<string, UserStory[]> = new Map<string, UserStory[]>();
public length(): number {
let cntr = 0;
this.users.forEach(() => cntr++);
return cntr;
}
public async download(base_dir: string): Promise<void> {
await Bluebird.map(this.users.entries(), async (entry) => {
const user = entry[0];
const story_list = entry[1];
const user_dir = path.join(base_dir, user);
await this.mkdir(user_dir);
await Bluebird.map(story_list, async (story) => {
const story_dir = path.join(user_dir, story.date_str);
await this.mkdir(story_dir);
await story.fetch(story_dir);
});
await this.dump_story_json(user_dir, story_list);
});
}
private async mkdir(dir: string): Promise<void> {
await new Promise((resolve) => {
fs.mkdir(dir, {recursive: true}, (err) => {
if (err)
console.error(err)
else
console.log(`Created directory ${dir}`)
resolve(null);
});
});
}
private async dump_story_json(dest_dir: string, stories: UserStory[]): Promise<void> {
const d = date.format(new Date, "YYYY-MM-DDTHHmmssZ", true);
const dest_path = path.join(dest_dir, `info_${d}.json`);
await new Promise((resolve) => {
fs.writeFile(dest_path, JSON.stringify(stories), (err) => {
if (err)
console.error(err)
else
console.log(`Written info to ${dest_path}`)
resolve(null);
});
});
}
}
export class Client
{
ig = new IgApiClient();
constructor(seed?: string) {
if (seed === undefined)
seed = `ig-story-dl-${date.format(new Date(), "YYYYMMDDHH")}`
this.ig.state.generateDevice(seed);
}
public async login(username: string) {
await this.ig.simulate.preLoginFlow();
const { passwd } = await inquirer.prompt([
{
type: 'password',
name: 'passwd',
message: `Enter password for ${username}`,
},
]);
await Bluebird.try(() => this.ig.account.login(username, passwd)).catch(
IgLoginTwoFactorRequiredError,
async err => {
const {username, totp_two_factor_on, two_factor_identifier} = err.response.body.two_factor_info;
const verificationMethod: string = totp_two_factor_on ? '0' : '0';
const { code } = await inquirer.prompt([
{
type: 'input',
name: 'code',
message: `Enter code received via ${verificationMethod === '1' ? 'SMS' : 'TOTP'}`,
},
]);
return this.ig.account.twoFactorLogin({
username,
verificationCode: code,
twoFactorIdentifier: two_factor_identifier,
verificationMethod, // '1' = SMS (default), '0' = TOTP (google auth for example)
trustThisDevice: '0',
});
},
).catch(e => console.error('An error occurred while processing two factor auth', e, e.stack));
console.log(`Logged in as ${username}`);
}
public async logout() {
await this.ig.account.logout();
console.log(`Logged out`);
}
public async get_stories(users: string[]): Promise<StoryCollection> {
let stories = new StoryCollection();
await Bluebird.map(users, async (user) => {
let user_stories: UserStory[] = [];
const target_user = await this.ig.user.searchExact(user);
const reels_feed = this.ig.feed.reelsMedia({
userIds: [target_user.pk]
});
console.info(`Fetch stories for ${target_user.pk} (${target_user.username})`)
const story_items = await reels_feed.items();
story_items.forEach((item) => {
console.info(`${target_user.pk} (${target_user.username}) story item: ${item.id}`)
if (item.image_versions2) {
item.image_versions2.candidates.forEach((pic) => {
user_stories.push(
new UserStory(
item.id,
item.taken_at,
pic.url
)
);
console.info(`${target_user.pk} (${target_user.username})->${item.id}: ${pic.url}`)
});
}
if (item.video_versions) {
item.video_versions.forEach((vid) => {
user_stories.push(
new UserStory(
item.id,
item.taken_at,
vid.url
)
);
console.info(`${target_user.pk} (${target_user.username})->${item.id}: ${vid.url}`)
});
}
});
stories.users.set(user, user_stories);
});
return stories;
}
}
export function json_replacer(key: any, value: any) {
if (value instanceof Map) {
return Array.from(value.entries());
} else {
return value;
}
}
export class Sleeper {
running: boolean = false;
poll_interval: number = 1000;
constructor(poll_interval: number = 1000) {
this.poll_interval = poll_interval;
}
public async sleep_ms(ms: number): Promise<void> {
this.running = true;
for (let remaining_ms = ms; (remaining_ms > 0) && this.running; remaining_ms -= this.poll_interval) {
await new Promise((resolve) => {
setTimeout(resolve, (remaining_ms > this.poll_interval) ? this.poll_interval : remaining_ms)
});
}
}
public cancel() {
this.running = false;
}
}