При работе с FB*, в особенности если речь о разбане после ЗРД (кто понял — тот понял), часто приходится уникализировать изображения. Однако на поверхностном изменении визуала уникализация не заканчивается — нужно еще и обновить метаданные.
Конечно, можно делать это специальным софтом — но для этого придется садиться за ПК, что бывает удобно далеко не всегда. Да и зачем использовать чей-то софт, если за пару минут можно сделать свой? Ниже расскажем, как сделать бот для Telegram, с помощью которого можно без особых запар изменять метаданные.
Какие задачи решает Telegram-бот для замены метаданных
Очевидно, что главной задачей бота является замена метаданных. В свою очередь, их замена через Telegram, может использоваться для:
уникализации изображений,
вывода из-под ЗРД в ПЗРД,
повышения траста при запуске РК,
обеспечения удобства мобильности работы.
Кроме перечисленного выше, бот может опционально использоваться для считывания и анализа метаданных: так как считывание является промежуточным этапом в процессе замены, в него добавлена возможность их вывода в чат.
Принцип работы Telegram-бота для замены метаданных
Полная алгоритмическая блок-схема бота достаточно сложная и довольно трудно поддается описанию из-за свойственного Telegram перекрестного переплетения функций. В особенности это заметно в частях кода, которые отвечают за вывод и работоспособность кнопочного меню.
Однако его принцип работы можно легко описать функционально — не упираясь в ограничения пошагового описания алгоритма. А именно:
- Прослушивание эфира и реакция на использование элементов управления
- Создание/редактирование/удаление пресетов метаданных (для массовой уникализации).
- Прослушивание эфира и реакция на способ отправки изображений.
- Непосредственно замена метаданных.
- Вывод изображения.
- Опционально — вывод вспомогательной информации (сами метаданные).
Как видите, если не упарываться в линейный пошаговый алгоритм, то бот устроен довольно просто для восприятия. Однако количество строк кода и отсутствие линейности все же намекают, что технически он сложен. Поэтому в случае внесения каких-либо правок обязательно делайте бекапы — очень большой шанс изменить что-то в одной части кода, а сломать другую.
В остальном же каких-либо трудностей с его использование возникнуть не должно. Во всяком случае при использовании в том виде, каким он подан ниже.
Пошаговая инструкция, как создать Telegram-бот для замены метаданных
Первым делом по классике нужно развернуть сервер, на котором будет находиться наш бот. Для этого можно использовать как арендные сервера, так и развернуть свой собственный сервер на Python. Затем нужно поочередно ввести в консоль:
pip install telegram
pip install piexif
pip install pillow
- Стучим в ЛС BotFather и следуем инструкции.
- Сохраняем токен.
- Открываем текстовый редактор, вставляем в него:
import os
import jsonimport telebot
import logging
from telebot import types
from PIL import Image
from PIL.ExifTags import TAGS
import exifread
import piexif
print ("Бот запущен")logging.basicConfig(level=logging.INFO)
token = "ТОКЕН СЮДА"
bot = telebot.TeleBot(token)
config_file = "config.txt"
img_folder = "img"
original_file = ""
new_file = ""
preset_directory = 'presets'
active_preset_path = os.path.join(preset_directory, "blank.json")
virtual_preset = {}
navigation_stack = []
user_state = {}
def load_json(filename): with open(filename, 'r', encoding='utf-8') as f:
return json.load(f)
def save_json(filename, data): with open(filename, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
def get_presets(): return [f.split('.')[0] for f in os.listdir(preset_directory) if f.endswith('.json') and f != 'blank.json']
def create_menu(options, back=True, save=False): markup = types.InlineKeyboardMarkup()
if back:
markup.add(types.InlineKeyboardButton("Назад", callback_data="back"))
if save:
markup.add(types.InlineKeyboardButton("Сохранить пресет", callback_data="save_preset"))
for option in options:
markup.add(types.InlineKeyboardButton(option, callback_data=option))
return markup
def create_select_menu(options): markup = types.InlineKeyboardMarkup()
for option in options:
markup.add(types.InlineKeyboardButton(option, callback_data=f"select_{option}"))
markup.add(types.InlineKeyboardButton("Назад", callback_data="back"))
return markup
def create_delete_menu(options): markup = types.InlineKeyboardMarkup()
for option in options:
markup.add(types.InlineKeyboardButton(option, callback_data=f"delete_{option}"))
markup.add(types.InlineKeyboardButton("Назад", callback_data="back"))
return markup
def extract_metadata(image_path): with open(image_path, 'rb') as f:
tags = exifread.process_file(f)
metadata = {}
for tag in tags.keys():
metadata[tag] = str(tags[tag])
return metadata
def print_metadata(metadata): ignore_tags = {'JPEGThumbnail'}
for tag, value in metadata.items():
if tag not in ignore_tags:
print(f"{tag}: {value}")
def update_exif(image_path, exif_data): try:
exif_dict = piexif.load(image_path)
except Exception as e:
raise RuntimeError(f"Ошибка при загрузке EXIF данных: {e}")
tag_dict = {
"0th": piexif.ImageIFD,
"Exif": piexif.ExifIFD,
"GPS": piexif.GPSIFD,
"Interop": piexif.InteropIFD,
"1st": piexif.ImageIFD
}
for ifd, tags in exif_data.items():
if ifd not in tag_dict:
print(f"IFD {ifd} не найден")
continue
for tag, info in tags.items():
if tag in tag_dict[ifd].__dict__:
tag_key = tag_dict[ifd].__dict__[tag]
value = info['value']
if value is None:
continue
if isinstance(value, list):
value = tuple(value)
elif isinstance(value, dict):
continue
if isinstance(value, tuple) and len(value) == 2 and isinstance(value[0], int) and isinstance(value[1], int):
value = (value[0], value[1])
elif isinstance(value, int):
value = int(value)
exif_dict[ifd][tag_key] = value
else:
print(f"Тег {tag} не найден в {ifd}")
try:
exif_bytes = piexif.dump(exif_dict)
except Exception as e:
raise RuntimeError(f"Ошибка при создании EXIF байтов: {e}")
return exif_bytes
def save_image_with_exif(original_image_path, new_image_path, exif_bytes): try:
img = Image.open(original_image_path)
img.save(new_image_path, exif=exif_bytes)
except Exception as e:
raise RuntimeError(f"Ошибка при сохранении изображения с EXIF данными: {e}")
def settings(call): config = read_config()
markup = types.InlineKeyboardMarkup()
markup.add(types.InlineKeyboardButton(f"Оригинал: {config['original'].capitalize()}", callback_data='toggle_original'))
markup.add(types.InlineKeyboardButton(f"Новый: {config['new'].capitalize()}", callback_data='toggle_new'))
markup.add(types.InlineKeyboardButton(f"Измененные: {config['changed'].capitalize()}", callback_data='toggle_changed'))
markup.add(types.InlineKeyboardButton("Назад", callback_data='back'))
bot.edit_message_text('Настройки отображения мета-данных:', call.message.chat.id, call.message.message_id, reply_markup=markup)
def read_config(): if not os.path.exists(config_file):
config = {
"original": "off",
"new": "off",
"changed": "on"
}
with open(config_file, 'w') as f:
json.dump(config, f)
else:
with open(config_file, 'r') as f:
config = json.load(f)
return config
def write_config(config): with open(config_file, 'w') as f:
json.dump(config, f)
def get_image_metadata(image_path): try:
with Image.open(image_path) as image:
exif_data = image._getexif()
metadata = {}
if exif_data:
for tag, value in exif_data.items():
tag_name = TAGS.get(tag, tag)
metadata[tag_name] = value
return metadata
except Exception as e:
return {"Error": str(e)}
def compare_metadata(meta1, meta2): differences = {}
for key in meta1.keys() | meta2.keys():
if meta1.get(key) != meta2.get(key):
differences[key] = f"{meta1.get(key)} -> {meta2.get(key)}"
return differences
def send_metadata(chat_id): config = read_config()
image1_path = os.path.join(img_folder, f"{original_file}")
image2_path = os.path.join(img_folder, f"{new_file}")
if not os.path.exists(image1_path) or not os.path.exists(image2_path):
bot.send_message(chat_id, "Одно или оба изображения не найдены в папке img.")
return
meta1 = get_image_metadata(image1_path)
meta2 = get_image_metadata(image2_path)
messages = []
if config['original'] == 'on':
messages.append("Оригинал:\n" + "\n".join(f"{k}: {v}" for k, v in meta1.items()))
if config['new'] == 'on':
messages.append("Новый:\n" + "\n".join(f"{k}: {v}" for k, v in meta2.items()))
if config['changed'] == 'on':
differences = compare_metadata(meta1, meta2)
if differences:
messages.append("Измененные:\n" + "\n".join(f"{k}: {v}" for k, v in differences.items()))
if messages:
bot.send_message(chat_id, "\n\n".join(messages))
else:
bot.send_message(chat_id, "Нет данных для отображения.")
def start(message): markup = create_menu(["Настроить пресет", "Выбрать пресет", "Удалить пресет", "Сбросить пресет", "Настройки"], back=False)
bot.send_message(message.chat.id, f"Активный пресет: {os.path.basename(active_preset_path)}", reply_markup=markup)
user_state.pop(message.chat.id, None)
def callback_query(call): global active_preset_path, virtual_preset, navigation_stack, user_state
try:
logging.info(f"Callback data received: {call.data}")
if call.data == "Настроить пресет":
virtual_preset = load_json(active_preset_path)
navigation_stack = [virtual_preset]
markup = create_menu(virtual_preset.keys(), back=True, save=True)
bot.edit_message_text(f"Активный пресет: {os.path.basename(active_preset_path)}", call.message.chat.id, call.message.message_id, reply_markup=markup)
elif call.data == "Настройки":
settings(call)
elif call.data.startswith('toggle_'):
setting = call.data.split('_', 1)[1]
config = read_config()
config[setting] = 'off' if config[setting] == 'on' else 'on'
write_config(config)
settings(call)
elif call.data == "Выбрать пресет":
presets = get_presets()
markup = create_select_menu(presets)
bot.edit_message_text("Выберите пресет:", call.message.chat.id, call.message.message_id, reply_markup=markup)
elif call.data == "Удалить пресет":
presets = get_presets()
markup = create_delete_menu(presets)
bot.edit_message_text("Удалите пресет:", call.message.chat.id, call.message.message_id, reply_markup=markup)
elif call.data.startswith("delete_"):
preset_to_delete = call.data.split("_", 1)[1]
confirm_markup = types.InlineKeyboardMarkup()
confirm_markup.add(types.InlineKeyboardButton("Да", callback_data=f"confirm_delete_{preset_to_delete}"))
confirm_markup.add(types.InlineKeyboardButton("Нет", callback_data="back"))
bot.edit_message_text(f"Вы уверены, что хотите удалить пресет {preset_to_delete}?", call.message.chat.id, call.message.message_id, reply_markup=confirm_markup)
elif call.data.startswith("confirm_delete_"):
preset_to_delete = call.data.split("_", 2)[2]
preset_path = os.path.join(preset_directory, f"{preset_to_delete}.json")
if os.path.exists(preset_path):
os.remove(preset_path)
if active_preset_path == preset_path:
active_preset_path = os.path.join(preset_directory, "blank.json")
virtual_preset = load_json(active_preset_path)
bot.edit_message_text(f"Пресет {preset_to_delete} удален.", call.message.chat.id, call.message.message_id)
presets = get_presets()
markup = create_delete_menu(presets)
bot.send_message(call.message.chat.id, "Удалите пресет:", reply_markup=markup)
else:
bot.edit_message_text(f"Пресет {preset_to_delete} не найден.", call.message.chat.id, call.message.message_id)
elif call.data.startswith("select_"):
preset_to_select = call.data.split("_", 1)[1]
active_preset_path = os.path.join(preset_directory, f"{preset_to_select}.json")
virtual_preset = load_json(active_preset_path)
bot.edit_message_text(f"Пресет {preset_to_select} выбран.", call.message.chat.id, call.message.message_id)
start(call.message)
elif call.data == "Сбросить пресет":
active_preset_path = os.path.join(preset_directory, "blank.json")
virtual_preset = load_json(active_preset_path)
bot.edit_message_text(f"Выбранный пресет сброшен", call.message.chat.id, call.message.message_id)
start(call.message)
elif call.data == "save_preset":
if os.path.basename(active_preset_path) == "blank.json":
msg = bot.send_message(call.message.chat.id, "Введите имя для нового пресета:")
user_state[call.message.chat.id] = {"state": "saving_new_preset"}
else:
save_json(active_preset_path, virtual_preset)
bot.edit_message_text(f"Пресет {os.path.basename(active_preset_path)} сохранен.", call.message.chat.id, call.message.message_id)
markup = create_menu(navigation_stack[-1].keys(), back=True, save=True)
bot.send_message(call.message.chat.id, f"Активный пресет: {os.path.basename(active_preset_path)}", reply_markup=markup)
elif call.data == "back":
if len(navigation_stack) > 1:
navigation_stack.pop()
current_menu = navigation_stack[-1]
markup = create_menu(current_menu.keys(), back=True, save=True)
bot.edit_message_text(f"Активный пресет: {os.path.basename(active_preset_path)}", call.message.chat.id, call.message.message_id, reply_markup=markup)
else:
start(call.message)
user_state.pop(call.message.chat.id, None)
elif call.data in navigation_stack[-1]:
item = navigation_stack[-1][call.data]
if isinstance(item, dict) and "value" in item and "type" in item:
msg = bot.send_message(call.message.chat.id, f"{call.data} ({item['type']}): {item['value']}\nОписание: {item['description']}\nПример: {item['example']}\nВведите новое значение:")
user_state[call.message.chat.id] = {"state": "updating_value", "key": call.data}
elif isinstance(item, dict):
navigation_stack.append(item)
markup = create_menu(item.keys(), back=True, save=True)
bot.edit_message_text(call.data, call.message.chat.id, call.message.message_id, reply_markup=markup)
else:
msg = bot.send_message(call.message.chat.id, f"{call.data}: {item}\nВведите новое значение:")
user_state[call.message.chat.id] = {"state": "updating_value", "key": call.data}
elif call.data.startswith("confirm_overwrite_"):
preset_name = call.data.split("_", 2)[2]
preset_path = os.path.join(preset_directory, preset_name)
try:
save_json(preset_path, virtual_preset)
active_preset_path = preset_path
bot.edit_message_text(f"Пресет {os.path.basename(active_preset_path)} перезаписан.", call.message.chat.id, call.message.message_id)
start(call.message)
except Exception as e:
logging.error(f"Ошибка при перезаписи пресета: {e}")
bot.send_message(call.message.chat.id, "Произошла ошибка при перезаписи пресета. Попробуйте снова.")
user_state.pop(call.message.chat.id, None)
elif call.data == "back_to_name_input":
msg = bot.send_message(call.message.chat.id, "Введите имя для нового пресета:")
user_state[call.message.chat.id] = {"state": "saving_new_preset"}
except IndexError:
bot.send_message(call.message.chat.id, "Ошибка: неверное состояние навигации.")
start(call.message)
def save_new_preset(message): global active_preset_path, virtual_preset
user_id = message.chat.id
user_state_info = user_state.get(user_id, {})
if user_state_info.get("state") == "saving_new_preset":
preset_name = message.text.strip() + ".json"
preset_path = os.path.join(preset_directory, preset_name)
if preset_name == "blank.json":
logging.info(f"User {user_id} tried to use invalid preset name: {preset_name}")
msg = bot.send_message(user_id, "Имя 'blank' нельзя использовать. Введите другое имя:")
bot.register_next_step_handler(msg, save_new_preset)
elif os.path.exists(preset_path):
confirm_markup = types.InlineKeyboardMarkup()
confirm_markup.add(types.InlineKeyboardButton("Да", callback_data=f"confirm_overwrite_{preset_name}"))
confirm_markup.add(types.InlineKeyboardButton("Нет", callback_data="back_to_name_input"))
bot.send_message(user_id, f"Пресет с именем '{preset_name}' уже существует. Перезаписать его?", reply_markup=confirm_markup)
user_state[user_id] = {"state": "confirming_overwrite", "preset_path": preset_path}
else:
try:
save_json(preset_path, virtual_preset)
active_preset_path = preset_path
bot.send_message(user_id, f"Новый пресет {os.path.basename(active_preset_path)} сохранен.")
start(message)
except Exception as e:
logging.error(f"Ошибка при сохранении пресета: {e}")
bot.send_message(user_id, "Произошла ошибка при сохранении пресета. Попробуйте снова.")
user_state.pop(user_id, None)
else:
logging.warning(f"Unexpected state for user {user_id}: {user_state_info.get('state')}")
def update_value(message): global virtual_preset
if user_state.get(message.chat.id, {}).get("state") == "updating_value":
key = user_state[message.chat.id]["key"]
value = message.text
if key in navigation_stack[-1]:
navigation_stack[-1][key]["value"] = value
virtual_preset = navigation_stack[0]
bot.send_message(message.chat.id, f"Значение {key} обновлено на {value}.")
markup = create_menu(navigation_stack[-1].keys(), back=True, save=True)
bot.send_message(message.chat.id, f"Активный пресет: {os.path.basename(active_preset_path)}", reply_markup=markup)
user_state.pop(message.chat.id, None)
@bot.message_handler(commands=['start'])def handle_start(message):
start(message)
@bot.callback_query_handler(func=lambda call: True)
def handle_callback_query(call):
callback_query(call)
@bot.message_handler(func=lambda message: user_state.get(message.chat.id, {}).get("state") == "saving_new_preset")
def handle_save_new_preset(message):
save_new_preset(message)
@bot.message_handler(func=lambda message: user_state.get(message.chat.id, {}).get("state") == "updating_value")
def handle_update_value(message):
update_value(message)
@bot.message_handler(content_types=['photo'])
def handle_photo(message):
bot.send_message(message.chat.id, "Пожалуйста, отправьте изображение как файл, а не как фото.")
@bot.message_handler(content_types=['document'])
def handle_document(message):
global original_file, new_file
file_info = bot.get_file(message.document.file_id)
downloaded_file = bot.download_file(file_info.file_path)
if not os.path.exists(img_folder):
os.makedirs(img_folder)
img_path = os.path.join(img_folder, "original_" + message.document.file_name)
with open(img_path, 'wb') as new_file_obj:
new_file_obj.write(downloaded_file)
exif_bytes = update_exif(img_path, virtual_preset)
new_img_path = os.path.join(img_folder, os.path.basename(img_path).lstrip("original_"))
save_image_with_exif(img_path, new_img_path, exif_bytes)
ext = os.path.splitext(message.document.file_name)[1]
original_file = os.path.splitext("original_" + message.document.file_name + ext)[0]
new_file = os.path.splitext(os.path.basename(img_path).lstrip("original_") + ext)[0]
with open(new_img_path, 'rb') as img_file:
bot.send_document(message.chat.id, img_file, caption="Измененное изображение")
send_metadata(message.chat.id)
bot.polling() - В строке token = "ТОКЕН СЮДА" подставляем токен.
- Сохраняем файл как bot.py.
- Создаем в корне сервера папку presets.
- В ней создаем файл blank.json.
- Вставляем в него:
{
"0th": { "ImageDescription": {
"type": "string",
"value": null,
"example": "A beautiful sunset",
"description": "Описание изображения"
},
"Make": {
"type": "string",
"value": "Canon",
"example": "Canon",
"description": "Производитель камеры"
},
"Model": {
"type": "string",
"value": "Canon EOS 80D",
"example": "Canon EOS 80D",
"description": "Модель камеры"
},
"Orientation": {
"type": "integer",
"value": null,
"example": 1,
"description": "Ориентация изображения"
},
"XResolution": {
"type": "rational",
"value": null,
"example": [
300,
1
],
"description": "Горизонтальное разрешение"
},
"YResolution": {
"type": "rational",
"value": null,
"example": [
300,
1
],
"description": "Вертикальное разрешение"
},
"Software": {
"type": "string",
"value": null,
"example": "Adobe Photoshop CC",
"description": "Программное обеспечение"
},
"DateTime": {
"type": "string",
"value": "2023:07:17 14:55:22",
"example": "2024:07:17 14:55:22",
"description": "Дата и время изменения"
},
"Artist": {
"type": "string",
"value": null,
"example": "John Doe",
"description": "Автор изображения"
}
},
"Exif": {
"ExposureTime": {
"type": "rational",
"value": null,
"example": [
1,
125
],
"description": "Выдержка"
},
"FNumber": {
"type": "rational",
"value": null,
"example": [
8,
1
],
"description": "Диафрагма"
},
"ExposureProgram": {
"type": "integer",
"value": null,
"example": 3,
"description": "Программа экспозиции"
},
"ISOSpeedRatings": {
"type": "integer",
"value": null,
"example": 100,
"description": "Чувствительность ISO"
},
"DateTimeOriginal": {
"type": "string",
"value": null,
"example": "2024:07:17 14:55:22",
"description": "Исходная дата и время"
},
"DateTimeDigitized": {
"type": "string",
"value": null,
"example": "2024:07:17 14:55:22",
"description": "Дата и время оцифровки"
},
"ShutterSpeedValue": {
"type": "rational",
"value": null,
"example": [
6935784,
1000000
],
"description": "Скорость затвора"
},
"ApertureValue": {
"type": "rational",
"value": null,
"example": [
6,
1
],
"description": "Значение диафрагмы"
},
"BrightnessValue": {
"type": "rational",
"value": null,
"example": [
4,
1
],
"description": "Яркость"
},
"ExposureBiasValue": {
"type": "rational",
"value": null,
"example": [
0,
1
],
"description": "Смещение экспозиции"
},
"MaxApertureValue": {
"type": "rational",
"value": null,
"example": [
4,
1
],
"description": "Максимальное значение диафрагмы"
},
"MeteringMode": {
"type": "integer",
"value": null,
"example": 5,
"description": "Режим замера экспозиции"
},
"Flash": {
"type": "integer",
"value": null,
"example": 0,
"description": "Состояние вспышки"
},
"FocalLength": {
"type": "rational",
"value": null,
"example": [
50,
1
],
"description": "Фокусное расстояние"
},
"SubSecTimeOriginal": {
"type": "string",
"value": null,
"example": "00",
"description": "Дробные секунды исходного времени"
},
"SubSecTimeDigitized": {
"type": "string",
"value": null,
"example": "00",
"description": "Дробные секунды времени оцифровки"
}
},
"GPS": {
"GPSLatitudeRef": {
"type": "string",
"value": null,
"example": "N",
"description": "Северная или южная широта"
},
"GPSLatitude": {
"type": "rational",
"value": null,
"example": [
[
37,
1
],
[
48,
1
],
[
3036,
100
]
],
"description": "Широта"
},
"GPSLongitudeRef": {
"type": "string",
"value": null,
"example": "W",
"description": "Восточная или западная долгота"
},
"GPSLongitude": {
"type": "rational",
"value": null,
"example": [
[
122,
1
],
[
28,
1
],
[
5760,
100
]
],
"description": "Долгота"
},
"GPSAltitudeRef": {
"type": "integer",
"value": null,
"example": 0,
"description": "Ссылка на высоту (над уровнем моря или ниже)"
},
"GPSAltitude": {
"type": "rational",
"value": null,
"example": [
100,
1
],
"description": "Высота над уровнем моря"
},
"GPSTimeStamp": {
"type": "rational",
"value": null,
"example": [
[
12,
1
],
[
34,
1
],
[
56,
1
]
],
"description": "Время GPS"
},
"GPSDateStamp": {
"type": "string",
"value": null,
"example": "2024:07:17",
"description": "Дата GPS"
}
},
"Interop": {
"InteroperabilityIndex": {
"type": "string",
"value": null,
"example": "R98",
"description": "Индекс интероперабельности"
},
"InteroperabilityVersion": {
"type": "string",
"value": null,
"example": "0100",
"description": "Версия интероперабельности"
}
},
"1st": {
"Compression": {
"type": "integer",
"value": null,
"example": 6,
"description": "Метод сжатия"
},
"JPEGInterchangeFormat": {
"type": "integer",
"value": null,
"example": 1234,
"description": "Начало JPEG данных"
},
"JPEGInterchangeFormatLength": {
"type": "integer",
"value": null,
"example": 5678,
"description": "Длина JPEG данных"
}
},
"thumbnail": {
"ImageWidth": {
"type": "integer",
"value": null,
"example": 160,
"description": "Ширина миниатюры"
},
"ImageLength": {
"type": "integer",
"value": null,
"example": 120,
"description": "Высота миниатюры"
},
"Compression": {
"type": "integer",
"value": null,
"example": 6,
"description": "Метод сжатия миниатюры"
},
"XResolution": {
"type": "rational",
"value": null,
"example": [
72,
1
],
"description": "Горизонтальное разрешение миниатюры"
},
"YResolution": {
"type": "rational",
"value": null,
"example": [
72,
1
],
"description": "Вертикальное разрешение миниатюры"
},
"ResolutionUnit": {
"type": "integer",
"value": null,
"example": 2,
"description": "Единица разрешения"
},
"ThumbnailOffset": {
"type": "integer",
"value": null,
"example": 23456,
"description": "Смещение миниатюры"
},
"ThumbnailLength": {
"type": "integer",
"value": null,
"example": 34567,
"description": "Длина миниатюры"
}
}
} - Сохраняем файл
- Создаем в корне сервера папку img
- Запускаем бот командой python bot.py
- Бот запущен и работает — проверяем бот на работоспособность.
* Чтобы убедиться в том, что бот действительно работает как задумано, советуем использовать не только встроенный функционал отображения метаданных, но и сторонние сайты.
Демонстрация работы
Подводя итоги
Создать собственный бот для замены метаданных — проще, чем кажется. Теперь можно танцевать с бубном при ЗРД и прочих радостях арбитража не только дома или в офисе, но и в дороге или в любимой кальянной. И с помощью Телеги это гораздо удобнее.