Skip to content

Commit

Permalink
1.0.0 release
Browse files Browse the repository at this point in the history
  • Loading branch information
K0nfy committed May 12, 2020
1 parent b9c331f commit 1d890ce
Show file tree
Hide file tree
Showing 37 changed files with 66,337 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Что это?
Асинхронный скрипт, основанный на промисах, который грузит картинки с локальной файловой директории в альбомы указанной группы [ВКонтакте](https://vk.com/) при помощи токенов редакторов группы.

## Установка

1. Скачать
2. `npm i` / `yarn install`

## Особенности работы
### tl;dr
У каждого аккаунта есть лимит на количество загружаемых картинок, поэтому не перезапускайте скрипт с одним и тем же токеном. Есть большой шанс нарваться на VK API flood control.
Автор поленился найти оптимальную задержку для загрузки более 5000 картинок одним токеном непрерывно, поэтому скрипт не запустится если будет обнаружена попытка загрузить слишком много картинок при малом количестве токенов.
Не забывайте: лимит одного альбома ВК - 10000.


### Подробно
При создании этого скрипта удалось выяснить примерную информацию о том как работают ограничения API ВКонтакте для загрузки картинок в альбомы групп.
Добавление картинок в альбомы через API состоит из двух этапов:
1. Загрузка картинки на сервер ВКонтакте;
2. Сохранение картинки в альбоме отдельным запросом.

Главное ограничение заключается в частоте запросов на сохранение картинок. Чем меньше частота, тем быстрее токен получит блокировку (Ошибка API №9 - flood control), которая длится неопределённый срок.
Я получал блокировки длительностью от нескольких минут до 8 часов. У вас этот срок может быть другим.
В этом скрипте картинки сохраняются по 25 штук за раз через `vk.api.execute`. 1 запрос - 25 сохранений.

К примеру, вам нужно загрузить 2000 картинок. Минимальная задержка между запросами для 1 токена, при которой возможно загрузить 2000 картинок без блокировки, - 15 секунд. После этих 2000 за 15 секунд
токен будет сразу же забанен и через него не получится больше ничего загрузить в ближайшие часы.
Если повысить задержку до 30 секунд, токен всё так же будет забанен спустя 2000 картинок.
При снижении задержки до 10, токен банится после 100 картинок.
При повышении до 65 - 5000 картинок.

То есть, существуют определённые контрольные точки задержек, после которых резко повышается количество загружаемых картинок до блокировки. Я составил таблицу задержек, которая сама применяется в скрипте.
Задержка будет выставлена в зависимости от того сколько картинок вы собираетесь загрузить. Пример условия - под таблицей.

Сейчас таблица выглядит так:
Задержка, секунд | Картинок до бана | Примерное затраченное время |
|--|--|--|
15 | 2000 | 20мин
45 | 2975 | 1ч 30мин
65 | 5000 | 3ч 40мин

Пример: если вы грузите от 2001 до 5000 картинок, задержка будет равна 65 секунд.
От 1 до 2000 картинок - 15 секунд.

Таблица не просчитана для загрузки более 5000 картинок одним токеном. Скрипт не запустится чтобы избежать
VK API flood control. Это поведение можно изменить задав задержку в `options.customDelays.constant`

Важно: если вы запустили загрузку, допустим, 2000 картинок, и прервали её на полпути, а затем запустили загрузку ещё 2000, токен уже не сможет загрузить их если сделать перезапуск не подождав какое-то неизвестное время. Скрипт не знает что токен использовался в прошлом запуске и попробует загрузить 2000 картинок с задержкой 15 секунд, но её уже будет недостаточно, и токен будет забанен.

## to do
- Консольный вывод: предположительное время окончания загрузки;
- Улучшить обработку ошибок, обрабатывать больше;
- Другие варианты логина в вк;
34 changes: 34 additions & 0 deletions classes/ApiCallers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

const VK = require('vk-io').VK;

module.exports = class ApiCallers {
constructor(tokens) {
const queue = this.recursiveQueue = tokens.slice();
const list = this.list = {}; // ApiCallers.list

for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
const entry = list[token] = {};

if (!/[a-zA-Z0-9]/.test(token)) { throw new Error(`Wrong token: ${token}`) }

entry.apiCaller = new VK();
entry.apiCaller.token = entry.token = token;

entry.lastSaveCallTime = -Infinity;
}

this.queueNext = 0;
}

next() {
const current = this.queueNext;
const keysList = this.recursiveQueue;
const currentKey = keysList[current];

if (++this.queueNext > keysList.length - 1) { this.queueNext = 0 }

return this.list[currentKey];
}
}
114 changes: 114 additions & 0 deletions classes/LoadQueue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use strict';

const fs = require('fs');
const util = require('util');
const fetch = require('node-fetch');
const FormData = require('form-data');

const fsAccess = util.promisify(fs.access);

module.exports = class LoadQueue {
constructor(imgsPath, albumId) {
this.imgsPath = imgsPath;
this.albumId = albumId;
}

async load({ items, loaders }) {
const formdatasPromises = [];

for (const item of items) {
formdatasPromises.push(await LoadQueue.makeFormdatas(item, this.imgsPath));
}

const formdatasChunks = await Promise.all(formdatasPromises);
const fetchChunks = [];

for (let i = 0; i < formdatasChunks.length; i++) {
const formdatas = formdatasChunks[i];
const loader = loaders.next();
const apiCaller = loader.apiCaller;
const uploadUrl = apiCaller.vkAlbPoster[this.albumId].uploadUrl;
const fetchChunk = fetchChunks[i] = { loader, fetches: [] };

for (const formdata of formdatas) {
fetchChunk.fetches.push(fetch(uploadUrl, {
method: 'POST',
body: formdata
}).then(res => res.json()));
}
}

const promiseAllArr = [];

for (const fetchChunk of fetchChunks) {
promiseAllArr.push(Promise.all(fetchChunk.fetches));
}

// promiseAll promiseAll
const responsesChunks = await Promise.all(promiseAllArr);

const splittedChunks = responsesChunks.map(chunk => {
const newChunk = [];

for (const item of chunk) {
const photos_list = JSON.parse(item.photos_list);

for (const photoObj of photos_list) {
const itemCopy = Object.assign({}, item);

itemCopy.photos_list = JSON.stringify([photoObj]);

newChunk.push(itemCopy);
}
}

return newChunk;
});

const output = [];

for (let i = 0; i < splittedChunks.length; i++) {
const resChunk = splittedChunks[i];
const inputChunk = items[i];

for (let x = 0; x < resChunk.length; x++) {
const resItem = resChunk[x];
const inputItem = inputChunk[x];

if (inputItem.hasOwnProperty('caption')) {
resItem.caption = inputItem.caption;
}
}

output.push({ loader: fetchChunks[i].loader, responses: resChunk });
}

return output;
}


static async makeFormdatas(items, imgsPath) {
const output = [];
const chunkedItems = [].concat.apply([],
items.map(function(elem, i) {
return i % 1 ? [] : [items.slice(i, i + 1)];
})); // divide by 1 due to VK restrictions (uploading photos with unique captions)

for (const chunk of chunkedItems) {
const formData = new FormData();
output.push(formData);

for (let i = 0; i < chunk.length; i++) {
const item = chunk[i];
const imgPath = imgsPath + '/' + item.filename;

await fsAccess(imgPath); // try to access

const buffer = fs.createReadStream(imgPath);
formData.append(`file${i + 1}`, buffer);
}
}

return output;
}
}
22 changes: 22 additions & 0 deletions classes/LogStream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const fs = require('fs');
const dateFormat = require('dateformat');

module.exports = class LogStream {
constructor(path) {
if (!path && path !== '') { return }

const currentDate = dateFormat(Date.now(), 'dd.mm.yyyy--HH-MM-ss');
const logFileName = `log--${currentDate}.json`;
const logFileFullPath = path + '/' + logFileName;

this.instance = fs.createWriteStream(logFileFullPath);
}

end() {
if (this.instance) { this.instance.end() }
}

write(data) {
if (this.instance) { this.instance.write(data) }
}
}
40 changes: 40 additions & 0 deletions example-caller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const VkAlbPoster = require('./index.js');

const vkAlbPoster = new VkAlbPoster({
group_id: '195251462', // ID группы
tokens: [ // токены аккаунтов, которые имеют права на загрузку в альбомы указанной группы
'cad9f50c062409b3eb815267bd878aa7b7be69051e37153783f112689a959624',
'59e8b6780ad7ac0784342b67b2fd345c2ef7d6c7d0b3b19cfb177968862067e2'
],
options: {
customDelays: { // в секундах
// Если вы столкнулись с 9 ошибкой API - flood control, попробуйте повысить это значение и
// сделать запуск с новым токеном другого аккаунта
// Токены аккаунта, который столкнулся с данной ошибкой, могут быть забанены API на несколько часов
// extra: 0, // По-умолчанию: 0. Рекомендуемый шаг - 30

// Укажите свою задержку вместо автоматической из таблицы (подробнее в документации)
// Складывается с extra
// constant: 100 // По-умолчанию: зависит от входных данных
}
}
});

vkAlbPoster
.post({
albums: [
{
name: 'test_10',
imgsPath: './input_example/60pcs/pics',
descrsPath: './input_example/60pcs/descriptions60.json',
logFilePath: './input_example/60pcs/' // если этот параметр указан, скрипт сделает лог-файл
},
// { // можно грузить сразу несколько альбомов! Грузится будут по очереди
// name: 'test_11',
// imgsPath: './input_example/1pc-1px',
// descrsPath: './input_example/1pc-1px/descriptions200.json'
// }
]
})
.then(console.log)
.catch(console.error);
Loading

0 comments on commit 1d890ce

Please sign in to comment.