-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
37 changed files
with
66,337 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
- Консольный вывод: предположительное время окончания загрузки; | ||
- Улучшить обработку ошибок, обрабатывать больше; | ||
- Другие варианты логина в вк; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Oops, something went wrong.