[Инструкция]Автопостинг на DTF. Все, что вы хотели знать об автопостиге, но боялись спросить

В этом посте мы изучим как делать автоматические посты на dtf.ru, научимся создавать процедурные гифки и освоим создание и запуск собственных сервисов на Windows на JS скриптах.

🔊

Оглавление

Допустим Вы являетесь фанатом серии Anno и горите желанием каждый день постить в своём бложике гифку с обратным отсчётом дней до релиза новой игры серии.

Типа такого. Не забудьте добавить тег для скрытия(не работает - не в приоритете. Хе-хе) чтобы не мучить тех кому эта игра не интересна

Что Вам для этого нужно? Можно по старинке наделать картинок или гифок ручками. И каждый день создавать новые посты через редактор Очобы.

Вариант номер 2 - автоматизировать этот процесс. Написать скрипт который всё это будет делать автоматически: делать гифку, заливать гифку, публиковать пост каждый день в определённое время.

Нам потребуется простой советский

1. Создаем папку проекта

Итоговый проект будет выглядеть вот так

СПОЙЛЕРЫ!
СПОЙЛЕРЫ!

2. Создаём подложки для гифок

Скачиваем ролик с YouTube и нарезаем его на 10-тисекундные сегменты при помощи FFMPEG. Выбираем 20 лучших клипов, нумеруем их от 0.mp4 до 19.mp4 и копируем их в папку videos проекта.

Всё ещё нарезаете клипы в After Effects? Тогда мы идём к Вам.

Код clips.bat:

ffmpeg -i %1 -acodec copy -f segment -segment_time 10 -vcodec copy -reset_timestamps 1 -map 0 "%%d.mp4"

3. Пишем скрипт для автопостинга "autopost.js"

const DTF_API = require("./src/dtf_api"); //библиотека для работы с API DTF const Util = require('util'); //стандартная библиотека для более красивого логирования const Sharp = require('sharp'); //библиотека для работы с растровыми картинками const Fs = require('fs'); //стандартная библиотека для работы с файлами const Path = require('path'); //стандартная библиотека для работы путями файлов const {registerFont, UltimateTextToImage} = require("ultimate-text-to-image"); //библиотека для генерации картинок с текстом const Ffmpeg_static = require('ffmpeg-static'); //ffmpeg для работы с видео const Ffmpeg = require('fluent-ffmpeg'); //библиотека для работы с ffmpeg Ffmpeg.setFfmpegPath(Ffmpeg_static); const FULL_AUTO = false; //true - полноавтоматический режим: постится по времени, false - постится при запуске const DTF_USER__LOGIN = 'XXXXXXXXXXX.XX'; //логин const DTF_USER__PASSWORD = 'XXXXXXXX'; //пароль const DTF_USER__USER_ID = 933181; //id пользователя const POST__SUBSITE_ID = 933181; //id подсайта куда будем постить. Тот же что и DTF_USER__USER_ID, если постим в личные блоги const GAME__TITLE = 'Anno 117: Pax Romana'; //название игры const GAME__MASHINE_NAME = 'anno_117'; //машинное название игры для тега const GAME__RELEASE_DATE = new Date('2025-11-13'); //дата релиза игры YYYY-MM-DD const POST__TIME_HOUR = 23; //время постинга - час const POST__TIME_MINUTE = 0; //время постинга - минута const POST__PUBLISH = false; //true-публикация в ленту, false-в черновики const VIDEOS__COUNT = 20; //кол-во видео в папке videos const TARGET_VIDEO_WIDTH = 592; //ширина видео 1280/720 ~= 854/480 ~= 592/336 const TARGET_VIDEO_HEIGHT = 336; //высота видео const TARGET_VIDEO_FPS = 30; const TARGET_VIDEO_CRT = 18; //"качество" кодирования [0-51] 0-lossless, 23-default const TARGET_VIDEO_ENCODE_PRESET = 'slower'; //пресет кодирования, время на задачу - ultrafast superfast veryfast faster fast medium slow slower veryslow const TARGET_VIDEO_BITRATE_MAX = '800K'; //максимальный битрейт const MSECS_IN_DAY = 86400000; //миллисекунд в дне: 24 * 60 * 60 * 1000; const log_file = Fs.createWriteStream(Path.join(__dirname, 'debug.log'), {flags : 'a'}); //логирование в файл const log_stdout = process.stdout; //логирование в консоль class Autopost { constructor() { this.main(); } log = function(d) { // log_file.write(`${get_time()} ${Util.format(d)}` + '\n'); log_stdout.write('Autopost: ' + Util.format(d) + '\n'); function get_time() { const dateObject = new Date(); const date = (`0${dateObject.getDate()}`).slice(-2); const month = (`0${dateObject.getMonth() + 1}`).slice(-2); const year = dateObject.getFullYear(); const hours = dateObject.getHours(); const minutes = (`0${dateObject.getMinutes()}`).slice(-2); const seconds = (`0${dateObject.getSeconds()}`).slice(-2); return (`${date}/${month}/${year} ${hours}:${minutes}:${seconds}`); } }; async main() { await this.autopost(); return true; } async autopost() { const inst = this; inst.log('autopost start'); //сколько дней до выхода игры const difference_in_ms = GAME__RELEASE_DATE.getTime() - new Date().getTime(); const difference_in_days = Math.ceil(difference_in_ms/(MSECS_IN_DAY)); //пути к временным ассетам const gen_assets = []; gen_assets['text_image'] = Path.join(__dirname, 'tmp', 'text_image.png'); gen_assets['video'] = Path.join(__dirname, 'tmp', 'video.mp4'); //генерируем плашку с текстом const gen_image_response = await inst.image_text_create(difference_in_days + '', {width:TARGET_VIDEO_WIDTH, height:TARGET_VIDEO_HEIGHT}, gen_assets['text_image']).catch((error)=>inst.log(error)); if (!gen_image_response) return false; const video_index = difference_in_days % VIDEOS__COUNT; const video = {filepath: Path.join(__dirname, 'videos', `${video_index}.mp4`)}; const image = {filepath: gen_assets['text_image'], width: gen_image_response.width, height: gen_image_response.height }; //генерируем видео const cb = await inst.video_clip_create(video, image, gen_assets['video']).catch((error)=>{inst.log(error)}); if (!cb) return; //инициализируем объект для работы с API DTF const dtf_api = new DTF_API(DTF_USER__LOGIN, DTF_USER__PASSWORD, inst.log); await dtf_api.auth(); //загружаем видео на DTF const upload_response = await dtf_api.uploader_upload(gen_assets['video'], 'video.mp4').catch(function(error){inst.log(error)}); if (!upload_response) return; if (!upload_response?.data?.result[0]) { inst.log(`!upload_response?.data?.result[0]`); return; } //публикуем пост на DTF const post_title = `До релиза ${GAME__TITLE} — ${difference_in_days} ${days_ending(difference_in_days)}`; function days_ending(days) { if (days === 1) return `день`; return (days===0 || days > 4) ? `дней` : `дня`; } const blocks = []; blocks.push({"type":"media","cover":true,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":upload_response.data.result[0]}]}}); blocks.push({"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":`<p>#${GAME__MASHINE_NAME}_counter</p>`}}); const $entry = { "user_id": DTF_USER__USER_ID, "type": 1, "subsite_id": POST__SUBSITE_ID, "title": post_title, "entry": { "blocks": blocks }, "external_access_link":"", //??? "path":null, //??? "is_editorial":false, //для редакции. Нет смысла менять "is_advertisement":false, //для редакции. Нет смысла менять "is_enabled_comments":true, //для редакции. Нет смысла менять "is_enabled_likes":true, //для редакции. Нет смысла менять "withheld":false, //для редакции. Нет смысла менять "is_enabled_ad":true, //для редакции. Нет смысла менять "is_holdonflash":false, //для редакции. Нет смысла менять "forced_to_mainpage":0, //для редакции. Нет смысла менять "is_holdonmain":false, //для редакции. Нет смысла менять "is_published":POST__PUBLISH, //опубликовать?: true-публикация в ленту, false-в черновики "is_adult":false, //18+ контент "repostId":null, //id поста для репоста "repostData":null //??? } const editor_cb = await dtf_api.editor($entry).catch((error)=>{inst.log(error)}); if (!editor_cb) return; inst.log('Пост успешно опубликован') } ///autopost //создание изображения async image_text_create(text, media_data, output_filepath) { return new Promise(async (resolve, reject) => { try { registerFont(Path.join(__dirname, 'fonts', 'impact.ttf')); const text_to_image = new UltimateTextToImage(text, { width: Math.floor(media_data.width * 0.3), height: Math.floor(media_data.height * 0.3), maxWidth: Math.floor(media_data.width * 0.6), maxHeight: Math.floor(media_data.height * 0.6), fontFamily: "Impact", fontColor: "#c7c7c7", fontSize: Math.floor((media_data.width + media_data.height) * 0.3), minFontSize: Math.floor(media_data.width * 0.2), autoWrapLineHeightMultiplier: 1.05, margin: Math.floor(media_data.width * 0.02), align: "center", valign: "middle", noAutoWrap: true, }); const text_buffer = text_to_image.render().toBuffer(); const stroke_size = Math.ceil((media_data.width + media_data.height) * 0.004); text_to_image.options.fontColor = "#262626"; const stroke_text_buffer = text_to_image.render().toBuffer(); let $sharp_buffer = stroke_text_buffer; for (let $angle = 0; $angle < 2 * Math.PI; $angle += 2 * Math.PI / 16) { $sharp_buffer = await Sharp($sharp_buffer) .composite([{ input: stroke_text_buffer, top: Math.round(stroke_size * Math.sin($angle)), left: Math.round(stroke_size * Math.cos($angle)), }]) .toBuffer(); } await Sharp($sharp_buffer).blur(Math.ceil(stroke_size / 8)) .composite([{ input: text_buffer }]) .toFile(output_filepath); return resolve({width: text_to_image.width, height: text_to_image.height}); } catch (error) { return reject(`image_text_create error: ${error}`); } }); } //создание видео наложением картие на видео async video_clip_create(video, image, output_filepath) { return new Promise(async (resolve, reject) => { try { Ffmpeg() .input(video.filepath) //входное видео [0] .input(image.filepath) //входная картинка [1] //.outputOptions([`-y`]) .outputOptions([`-c:v libx264`]) //видеокодек .outputOptions([`-preset ${TARGET_VIDEO_ENCODE_PRESET}`]) //пресет кодироваия .outputOptions([`-crf ${TARGET_VIDEO_CRT}`]) //качество кодирования .outputOptions([`-maxrate ${TARGET_VIDEO_BITRATE_MAX}`]) //максимальный битрейт .outputOptions([`-bufsize 2M`]) //максимальый размер буфера .complexFilter([ //масштабируем видео под заданный размер `[0:v] scale=${TARGET_VIDEO_WIDTH}:${TARGET_VIDEO_HEIGHT}:force_original_aspect_ratio=decrease,pad=${TARGET_VIDEO_WIDTH}:${TARGET_VIDEO_HEIGHT}:-1:-1:color=black,fps=${TARGET_VIDEO_FPS} [new0]`, //накладываем картинку над видео по центру `[new0][1:v] overlay=${Math.floor((TARGET_VIDEO_WIDTH - image.width) / 2)}:${Math.floor((TARGET_VIDEO_HEIGHT - image.height) / 2)}` ]) .outputOptions([`-an`]) //удаляем звук .output(output_filepath) //выходной файл .on('end', function () { return resolve(true); }) .on('error', function (error) { return reject(`video_clip_create error: ${error}` ); }) .run(); } catch (error) { reject(`video_clip_create: ${error}`); } }); } } (() => { //ручной запуск if (!FULL_AUTO) { new Autopost(); } //автопост каждый день в определенное время if (FULL_AUTO) { execute_at_time(POST__TIME_HOUR, POST__TIME_MINUTE, () => {new Autopost();}); //запуск функции каждый день в определенное время function execute_at_time(hour, minute, func) { const now = new Date(); const target_time = new Date(); target_time.setHours(hour, minute, 0, 0); let delay = target_time.getTime() - now.getTime(); if (delay < 0) { delay += MSECS_IN_DAY; } setTimeout(function () { func(); setInterval(func, MSECS_IN_DAY); }, delay); } } })();
Когда в редакторе сделают сворачиваемые блоки. А? А? Ненависть....

Что делает скрипт? При запуске создаётся объект класса Autopost, который:

  1. Считает сколько дней до выхода игры
  2. Генерирует прозрачную картинку(png) с обводкой с числом дней
  3. Генерирует "гифку" наложением png на клип из папки videos
  4. Загружает "гифку" на DTF
  5. Публикует пост на DTF

4. Разбираемся с блоками редактора

Формат блоков постов на dtf можно посмотреть при помощи инструментов разработчика браузера при работе в редакторе

[Инструкция]Автопостинг на DTF. Все, что вы хотели знать об автопостиге, но боялись спросить
[ //текст {"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"<p>текст текст текст</p>"}}, //заголовок H2 {"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Заголовок H2"}}, //заголовок H3 {"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"Заголовок H3"}}, //медиа: картинки и гифки {"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"Опиа","image":{"type":"image","data":{"uuid":"1bb37cac-a01b-5e49-b0e6-c980d296e14a","width":890,"height":632,"size":241497,"type":"png","color":"a98a69","hash":"","external_service":[],"isVideo":false,"duration":null,"has_audio":false}}}]}}, //ссылка {"type":"link","cover":false,"hidden":false,"anchor":"","data":{"link":{"type":"link","data":{"url":"http://google.com","title":"Google","description":"Поиск информации в интернете: веб страницы, картинки, видео и многое другое.","image":{"type":"image","data":{"uuid":"https://leonardo.osnova.io/ico/google.com","width":0,"height":0,"size":0,"type":"jpg","color":"","hash":"","external_service":[]}},"v":1,"hostname":"google.com"}}}}, //цитата {"type":"quote","cover":false,"hidden":false,"anchor":"","data":{"text":"<p data-placeholder=\"Текст цитаты\">Цитата<br></p><br>","subline1":"подпись"}}, //ненумерованный список {"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["список 1","список 2"],"type":"UL"}}, //нумерованный список {"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["список 1","список 2"],"type":"OL"}}, //код {"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"код","lang":""}}, //разделитель {"type":"delimiter","cover":false,"hidden":false,"anchor":"","data":{"type":"default"}}, //аудио {"type":"audio","cover":false,"hidden":false,"anchor":"","data":{"title":"","hash":"","audio":{"type":"audio","data":{"uuid":"03c49801-7543-591e-9a32-b74116794ce7","filename":"videoplayback.mp3","size":284976,"audio_info":{"bitrate":64000,"duration":35.76163265306123,"channel":"stereo","framesCount":0,"format":"mp3","listens_count":0}}}}}, //опросник {"type":"quiz","cover":false,"hidden":false,"anchor":"","data":{"hash":"ZtwIABkgQ2JEzsOZ0OXiL","title":"Заголовок опроса","items":{"o17495496600":"Вариант ответа 1","o17495496811":"Вариант ответа 1"}}}, //персона {"type":"person","cover":false,"hidden":false,"anchor":"","data":{"image":{"type":"image","data":{"uuid":"ac769ec1-a83e-5cdc-a077-d214d2fc3519","width":1019,"height":825,"size":104232,"type":"jpg","color":"4b3327","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAQDAwQDAwQEAwQFBAQFBgoHBgYGBg0JCggKDw0QEA8NDw4RExgUERIXEg4PFRwVFxkZGxsbEBQdHx0aHxgaGxr/2wBDAQQFBQYFBgwHBwwaEQ8RGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhr/wAARCAAKAAoDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAwYHCP/EACUQAAEDAwMDBQAAAAAAAAAAAAECAxEABAUGEiEHFCIxM1GRsf/EABQBAQAAAAAAAAAAAAAAAAAAAAX/xAAZEQACAwEAAAAAAAAAAAAAAAABAgADERP/2gAMAwEAAhEDEQA/AFNGtlX+scgbS+trbEMOntd5JU+neSrieOCYM+kVdcf1a0Nb2FqyrG3iy2yhJUGWzMCJndWDsS852/uK5SZ8j8kflGLi5Pkr7oZ1boTseUqaVGT/2Q==","isVideo":false,"duration":null,"has_audio":false}},"title":"Имя","description":"Должность"}} ]

Описание полей в блоках:

  • type - тип блока: text, media, header
  • cover - обложка: выводить ли блок в ленте. В ленте выводится 2 блока
  • hidden - спрятанный: блок будет спрятан под спойлер
  • anchor - якорь: можно поставить метку и ссылаться на неё допустим ссылкой в оглавлении
  • data - данные: по контексту блока

5. Полуавтоматический и полноавтоматический режимы работы

При параметре

const FULL_AUTO = false;

Cкрипт создаёт пост при запуске, после чего завершает свою работу. В папке проекта можно сделать bat файл "autopost.bat" с кодом:

node autopost.js

И надо будет каждый день его запускать. ¯\_(ツ)_/¯

Если нужен реальный автопостинг без Вашего участия, то измените параметр

const FULL_AUTO = true;

Скрипт при запуске будет выполнять создание нового поста каждый день в заданное время. Скрипт можно запустить на удаленном сервере или на рабочем компьютере.

6. Создаем и запускаем сервис в Windows

Чтобы не запускать на компьютере скрипт каждый день можно создать и запустить сервис.

const Path = require('path'); const Service = require('node-windows').Service; const svc = new Service({ name: '__DTFAnno117Autopost', description: 'DTF Anno 117 autoposting', script: Path.join(__dirname, 'autopost.js') }); svc.on('install', function() { svc.start(); }); svc.install();

Перед запуском сервиса убедитесь что скрипт настроен на полноавтоматический режим.

const FULL_AUTO = true;

Обязательно проверьте что сервис появился и запущен в диспетчере задач

[Инструкция]Автопостинг на DTF. Все, что вы хотели знать об автопостиге, но боялись спросить

Исходники и ассеты на моём "github":

После скачивания установите модули нажав install.bat, пропишите в autopost.js данные своей учётки(пошутите в комментах) и запустите autopost.bat.
В Ваших черновиках должен будет появится пост.

На этом всё, котаны. Пишите, если есть вопросы. И на какую тему мне ещё написать?

Если Вы всё прочитали и всё поняли, то Вы получаете ачивку! :

Если Вы ничего не читали или ничего не поняли:

Стыдно!
Стыдно!
10
7
2
2
1
1
1
33 комментария