Traffic Cardinal Traffic Cardinal написал 20.08.2024

Как создать Telegram-бот для замены метаданных

Traffic Cardinal Traffic Cardinal написал 20.08.2024
21 мин
0
901
Содержание

При работе с FB*, в особенности если речь о разбане после ЗРД (кто понял — тот понял), часто приходится уникализировать изображения. Однако на поверхностном изменении визуала уникализация не заканчивается — нужно еще и обновить метаданные.

banner banner

Конечно, можно делать это специальным софтом — но для этого придется садиться за ПК, что бывает удобно далеко не всегда. Да и зачем использовать чей-то софт, если за пару минут можно сделать свой? Ниже расскажем, как сделать бот для Telegram, с помощью которого можно без особых запар изменять метаданные.

Какие задачи решает Telegram-бот для замены метаданных

Очевидно, что главной задачей бота является замена метаданных. В свою очередь, их замена через Telegram, может использоваться для:

  • уникализации изображений,

  • вывода из-под ЗРД в ПЗРД,

  • повышения траста при запуске РК,

  • обеспечения удобства мобильности работы.

Кроме перечисленного выше, бот может опционально использоваться для считывания и анализа метаданных: так как считывание является промежуточным этапом в процессе замены, в него добавлена возможность их вывода в чат.

Принцип работы Telegram-бота для замены метаданных

Полная алгоритмическая блок-схема бота достаточно сложная и довольно трудно поддается описанию из-за свойственного Telegram перекрестного переплетения функций. В особенности это заметно в частях кода, которые отвечают за вывод и работоспособность кнопочного меню.

Однако его принцип работы можно легко описать функционально — не упираясь в ограничения пошагового описания алгоритма. А именно:

  1. Прослушивание эфира и реакция на использование элементов управления
  2. Создание/редактирование/удаление пресетов метаданных (для массовой уникализации).
  3. Прослушивание эфира и реакция на способ отправки изображений.
  4. Непосредственно замена метаданных.
  5. Вывод изображения.
  6. Опционально — вывод вспомогательной информации (сами метаданные).

Как видите, если не упарываться в линейный пошаговый алгоритм, то бот устроен довольно просто для восприятия. Однако количество строк кода и отсутствие линейности все же намекают, что технически он сложен. Поэтому в случае внесения каких-либо правок обязательно делайте бекапы — очень большой шанс изменить что-то в одной части кода, а сломать другую.

В остальном же каких-либо трудностей с его использование возникнуть не должно. Во всяком случае при использовании в том виде, каким он подан ниже.

Пошаговая инструкция, как создать Telegram-бот для замены метаданных

Первым делом по классике нужно развернуть сервер, на котором будет находиться наш бот. Для этого можно использовать как арендные сервера, так и развернуть свой собственный сервер на Python. Затем нужно поочередно ввести в консоль:

pip install telegram

pip install piexif

pip install pillow

На этом установка библиотек заканчивается, далее нужно получить токен.
На этом установка библиотек заканчивается, далее нужно получить токен.

  1. Стучим в ЛС BotFather и следуем инструкции.
  2. Сохраняем токен.
  3. Открываем текстовый редактор, вставляем в него:
    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()
  4. В строке token = "ТОКЕН СЮДА" подставляем токен.
  5. Сохраняем файл как bot.py.
  6. Создаем в корне сервера папку presets.
  7. В ней создаем файл blank.json.
  8. Вставляем в него:
    {
    "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": "Длина миниатюры"
    }
    }
    }
  9. Сохраняем файл
  10. Создаем в корне сервера папку img
  11. Запускаем бот командой python bot.py
  12. Бот запущен и работает — проверяем бот на работоспособность.

* Чтобы убедиться в том, что бот действительно работает как задумано, советуем использовать не только встроенный функционал отображения метаданных, но и сторонние сайты.

Демонстрация работы

Запускаем бот. Видим, что все типы метаданных разбиты по категориям. Это сделано для удобства (можно изменить структуру пресетов на свою и убрать все это)
Запускаем бот. Видим, что все типы метаданных разбиты по категориям. Это сделано для удобства (можно изменить структуру пресетов на свою и убрать все это)

Также видим, что каждый тип метаданных подписан с примерами — это тоже зависит лишь от содержания пресета и сделано для удобства. По этой же причине удаление/изменение пресета blank.json через интерфейс бота запрещено технически (чтобы шпаргалка по метаданным всегда была под рукой)
Также видим, что каждый тип метаданных подписан с примерами — это тоже зависит лишь от содержания пресета и сделано для удобства. По этой же причине удаление/изменение пресета blank.json через интерфейс бота запрещено технически (чтобы шпаргалка по метаданным всегда была под рукой)

Допустим, нам важно, чтобы в метаданные выглядели так, будто бы фото было сделано на iPhone. Меняем производителя гаджета и его модель. (Разумеется, заменить только модель и производителя недостаточно — но если мы будем менять все, то здесь будет 100500 скринов. Но менять вручную каждый раз не придется — ведь есть пресеты)
Допустим, нам важно, чтобы в метаданные выглядели так, будто бы фото было сделано на iPhone. Меняем производителя гаджета и его модель. (Разумеется, заменить только модель и производителя недостаточно — но если мы будем менять все, то здесь будет 100500 скринов. Но менять вручную каждый раз не придется — ведь есть пресеты)

После внесения всех нужных нам правок сохраняем пресет. Для удобства он автоматически становится активным после сохранения. Пока тестируем — ставим отображать все в настройках. Закидываем фото. Бот ругается, что это фото, а не файл — это вынужденная защита от изменения метаданных самой Телегой. Видим, что оно сделано на Samsung (модель и версия ПО скрыты в целях безопасности)
После внесения всех нужных нам правок сохраняем пресет. Для удобства он автоматически становится активным после сохранения. Пока тестируем — ставим отображать все в настройках. Закидываем фото. Бот ругается, что это фото, а не файл — это вынужденная защита от изменения метаданных самой Телегой. Видим, что оно сделано на Samsung (модель и версия ПО скрыты в целях безопасности)

Ниже по логу видим, что у нового файла производитель гаджета — Apple, а его модель — iPhone 15 Pro Max. Но допустим, мы уже набили руку, точно знаем, что хотим поменять и нам не нужны огромные логи. Отключаем в настройках вывод инфы об оригинальном и новом файлах — оставляя лишь непосредственно список изменений
Ниже по логу видим, что у нового файла производитель гаджета — Apple, а его модель — iPhone 15 Pro Max. Но допустим, мы уже набили руку, точно знаем, что хотим поменять и нам не нужны огромные логи. Отключаем в настройках вывод инфы об оригинальном и новом файлах — оставляя лишь непосредственно список изменений

Закидываем новое фото — теперь отчет лаконичнее (модель гаджета из оригинальных метаданных скрыта в целях безопасности)
Закидываем новое фото — теперь отчет лаконичнее (модель гаджета из оригинальных метаданных скрыта в целях безопасности)

Чтобы окончательно убедиться в том, что все работает как надо — кидаем оба файла на любой сайт для анализа метаданных онлайн. Видим, что их метаданные действительно поменялись
Чтобы окончательно убедиться в том, что все работает как надо — кидаем оба файла на любой сайт для анализа метаданных онлайн. Видим, что их метаданные действительно поменялись

Подводя итоги

Создать собственный бот для замены метаданных — проще, чем кажется. Теперь можно танцевать с бубном при ЗРД и прочих радостях арбитража не только дома или в офисе, но и в дороге или в любимой кальянной. И с помощью Телеги это гораздо удобнее.

Здравствуйте! У вас включен блокировщик рекламы, часть сайта не будет работать!