# Плагины сабмита

# Общая концепция

Отправка файлов на микросток в программе IMS Studio представляет собой выполнение заданного процесса над файлами. Сам процесс - это набор взаимосвязанных шагов, через которые проходит каждый из отправляемых на микросток файлов.

Типичный процесс сабмита состоит из следующих шагов:

  1. Проверить, что файлы подходят для загрузки
  2. Выполнить авторизацию на сайте микростока
  3. Загрузить файлы на микросток, если они еще не были загружены
  4. Подождать немного времени, чтобы загруженные файлы успели появиться на сайте микростока
  5. Выполнить сабмит:
    • найти загруженные файлы на микростоке
    • загрузить на микросток связанные релизы
    • сохранить метаданные файлов на микростоке
    • нажать на кнопку сабмита

Первый шаг выполняется с помощью системы фильтров - через условия фильтров проверяется, что файлы соответсвуют заданному направлению сабмита.

Второй шаг выполняется с помощью открытия окна встроенного браузера. Обычно сперва делается попытка открыть внутреннюю страницу личного кабинета автора (целевая страница). Если она перенаправляет браузер на форму входа, то эта форма отображается пользователю. Он вносит свои данные, и далее, как только пользователь попадает на целевую страницу, считается, что авторизация пройдена.

Третий шаг обычно выполняется через FTP/FTPS/SFTP (хотя ничто не ограничивает вас в способе реализации этого шага)

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

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

# Структура плагина сабмита

Для плагина сабмита нам нужно создать плагин, содержищий три сущности:

  1. destination - микросток
  2. ftp - FTP-соединение
  3. submit - сам процесс сабмита

Структура файлов проекта будет следующей:

my-plugin
|- index.json              -- описание структуры плагина
|- icon.png                -- иконка плагина
|- my-destination.json     -- содержимое плагина: микросток
|- my-ftp.json             -- содержимое плагина: FTP-соединение
|- my-submit.json          -- содержимое плагина: сабмит 
|- my-submit-code.js       -- JS-код сабмита
|- my-submit-dict.json     -- словари для JS-кода (необяз.)

Пример содержимого index.json:

{
    "name": "my-plugin",
    "title": "My plugin",
    "version": "1.0.0",
    "authors": "Me",
    "description": "Submit to my microstock",
    "website": null,
    "icon": { "@urlfile": "icon.png" },
    "content": [
        { "@jsonfile": "my-destination.json" },
        { "@jsonfile": "my-ftp.json" },
        { "@jsonfile": "my-submit.json" }
    ],
    "api": "2.4.0",
    "locale": {
    }
}

TIP

Полный пример плагина вы можете скачать по ссылке, приведенной в разделе Шаблон плагина

# Содержимое плагина: микросток

Структура описания микростока следующая:

{
    "type": "destination",                      // 1. Тип сущности
    "name": "my-destination",                   // 2. Служебное название
    "title": "My destination",                  // 3. Отображаемое имя
    "icon": "https://example.com/favicon.ico",  // 4. Иконка
    "content": {                                
        "maxNumberKeywords": 50,                // 5. Лимит ключевых слов (необяз.)
        "maxNumberCharsInTitle": 80,            // 6. Лимит символов в названии (необяз.)
        "maxNumberCharsInDesc": 200,            // 7. Лимит символов в описании (необяз.)
        "additionalProps": []                   // 8. Доп. свойства файлов для
                                                //      этого микростока  (необяз.)
    },
    "locale": {                                 // 9. Локализованные строки
    }                                           //    (см. раздел "Интернационализация")
}
  1. type - тип содержимого должен быть destination
  2. name - уникальное имя микростока. Если несколько плагинов реализуют один и тот же микросток, их описание будет объединено в одно
  3. title - отображаемое название микростока
  4. icon - ссылка на иконку микростока (обычно favicon сайта микростока)
  5. maxNumberKeywords - максимальное кол-во ключевых слов. Метаданные будут автоматически обрезаны до этого количества. Число или null.
  6. maxNumberCharsInTitle - максимальное кол-во символов в названии. Метаданные будут автоматически обрезаны до этого количества. Число или null.
  7. maxNumberCharsInDesc - максимальное кол-во символов в описании. Метаданные будут автоматически обрезаны до этого количества. Число или null.
  8. additionalProps - дополнительные поля метаданных для этого микростока. Например:
{
    // ...
    "content": { 
        // ...
        "additionalProps": [
            {
                "name": "myPrice",
                "title": "Цена",
                "type": "integer"    // Ввод целого числа 
            }
        ]
    }
    // ...
}

При выборе соответвующего набора метаданных в секции дополнительных свойст появится дополнительное поле "Цена" (а в коде сабмита будет доступно по имени myPrice).

Больше о создании дополнительных полей вы можете узнать здесь

  1. locale - служит для создания локализованных плагинов, поддерживающих несколько языков интерфейсов (см. раздел Интернационализация)

Пример файла my-destination.json

{
    "type": "destination",                     
    "name": "my-destination",                   
    "title": "My destination",                
    "icon": null, 
    "content": {       
        "maxNumberKeywords": 50
    },
    "locale": {                                
    }                                          
}

# Содержимое плагина: ftp

Структура описания FTP-соединения следующая:

{
    "type": "ftp",                              // 1. Тип сущности
    "name": "my-ftp",                           // 2. Служебное название
    "title": "My ftp",                          // 3. Отображаемое имя
    "icon": "https://example.com/favicon.ico",  // 4. Иконка
    "content": {
        "protocol": "ftps",
        "port": null,
        "host": "example.com",
        "formats": {
            "photo": {
                "enabled": true,
                "folder": null,
                "additional": []
            },
            "illustration": {
                "enabled": true,
                "type": "jpg",
                "folder": null,
                "previewFolder": null,
                "additional": []
            },
            "vector": {
                "enabled": true,
                "type": "zip",
                "ext": [
                    "eps"
                ],
                "folder": null,
                "previewExt": [
                    "jpg"
                ],
                "previewFolder": null,
                "additional": []
            },
            "video": {
                "enabled": true,
                "type": "video",
                "ext": [
                    "mov",
                    "mp4"
                ],
                "folder": null,
                "previewFolder": null,
                "additional": []
            }
        },
        "folder": null,
        "translitirateFilenames": true,
        "saveMplusCompat": false,
        "destinationName": "my-destination",    // 5. Микросток
        "usernameField": "email",               // 6. Название поля логина
        "note": {                               // 7. Доп. подсказки
            "en": "Some additional notes",
            "ru": "Какая-то доп. подсказка"
        }
        },
    "locale": {                                 // 8. Локализованные строки
    }                                           //    (см. раздел "Интернационализация")
}
  1. type - тип содержимого должен быть ftp
  2. name - уникальное имя FTP-соединения
  3. title - отображаемое название FTP-соединения
  4. icon - ссылка на иконку FTP-соединения (обычно favicon сайта микростока)

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

После нажатия настройки будут скопированы в буфер обмена. Вставьте значение в поле content. Обратите внимание только на следующие поля:

  1. destination - микросток, к которому будет привязан статус загрузки. Используйте то же имя, которые вы использовали в описании микростока из предыдущего раздела
  2. usernameField - название поля для ввода логина. Влияет исключительно на отображение формы ввода логина и пароля. Доступны следующие значения:
    • username - Имя пользователя (значение по умолчанию)
    • email- Email
    • id - ID
    • ftpLogin - FTP логин
  3. note - дополнительная подсказка, отображаемая пользователю. Задается на нескольких языках в соответвующих полях.
  4. locale - служит для создания локализованных плагинов, поддерживающих несколько языков интерфейсов (см. раздел Интернационализация)

Пример файла my-ftp.json

{
    "type": "ftp", 
    "name": "my-ftp",
    "title": "My ftp",  
    "icon": null,
    "content": {
        "protocol": "ftps",
        "port": null,
        "host": "example.com",
        "formats": {
            "photo": {
                "enabled": true,
                "folder": null,
                "additional": []
            },
            "illustration": {
                "enabled": true,
                "type": "jpg",
                "folder": null,
                "previewFolder": null,
                "additional": []
            },
            "vector": {
                "enabled": true,
                "type": "zip",
                "ext": [
                    "eps"
                ],
                "folder": null,
                "previewExt": [
                    "jpg"
                ],
                "previewFolder": null,
                "additional": []
            },
            "video": {
                "enabled": true,
                "type": "video",
                "ext": [
                    "mov",
                    "mp4"
                ],
                "folder": null,
                "previewFolder": null,
                "additional": []
            }
        },
        "folder": null,
        "translitirateFilenames": true,
        "saveMplusCompat": false,
        "destination": "my-destination"
	},
    "locale": { 
    }
}

# Содержимое плагина: сабмит

Самая важная часть - описание процесса сабмита. Этот элемент плагина будет отображаться в секции Сабмит при отправке файлов

Структура описания сабмита следующая:

{
    "type": "submit",                           // 1. Тип сущности
    "name": "my-submit",                        // 2. Служебное название
    "title": "My Submit",                       // 3. Отображаемое имя
    "description": "My description",            // 4. Описание сабмита (необяз.)
    "icon": "https://example.com/favicon.ico",  // 5. Иконка
    "content": {
        "destinationName": "my-destination",    // 6. Связанный микросток
        "properties": [                         // 7. Настраиваемые свойства
            // ...                              //    сабмита (необяз.)
        ],
        "process": {                            // 8. Описание процесса
            // ...
        },
    },
    "locale": {                                 // 9. Локализованные строки
    }                                           //    (см. раздел "Интернационализация")
}
  1. type - тип содержимого должен быть submit
  2. name - уникальное имя процесса сабмита
  3. title - отображаемое название процесса сабмита
  4. description - описание процесса сабмита (отображается при открытии перечня содержимого плагина)
  5. icon - ссылка на иконку процесса сабмита (обычно favicon сайта микростока)
  6. destinationName - микросток, к которому относится сабмит
  7. properties - перечень свойств сабмита. Отображаются здесь:

Для задания этих двух полей необходимо написать:

{
    // ...
    "properties": [
        {
            "name": "useDescriptionAsTitle",           // Служебное имя
            "type": "boolean",                         // Тип поля
            "default": true,                           // Начальное значение
            "title": "Использовать описание в [...]"   // Отображаемое имя
        },
        {
            "name": "clickSubmit",
            "type": "boolean",
            "default": true,
            "title": "Нажать кнопку сабмита", 
            "tooltip": "..."                           // Подсказка 
        }   
    ]
    // ...
}

Подробнее про формат задания полей - см. раздел Справка: генерация форм

  1. process - описание процесса - см. следующий раздел.
  2. locale - служит для создания локализованных плагинов, поддерживающих несколько языков интерфейсов (см. раздел Интернационализация)

# Описание процесса

Процесс описывается следующей структурой:

{
    "title": "Сабмит на My destination",        // 1. Название процесса
    "icon": "https://example.com/favicon.ico",  // 2. Иконка процесса
    "assetTypes": [                             // 3. Поддерживаемые процессом
        "photo",                                //    типы файлов
        "illustration",
        "vector",
        "video"
    ],
    "actions": [                                // 4. Перечень шагов процесса
        // ...                                  
    ]
}
  1. title - отображаемое имя процесса в списке активных процессов
  2. icon - отображаемая иконка процесса в списке активных процессов
  3. assetTypes - поддержиываемые типы файлов. Массив из следующих значений:
    • photo - фотографии
    • illustration - иллюстрации
    • vector - векторы
    • video - видео
    • release - релизы
    • source - исходники, RAW-файлы
  4. actions - перечень блоков (шагов) процесса. Первый блок является начальным

Каждый блок процесса описывается следующим объектом:

{
    "name": "action1",         // 1. Имя блока процесса. 
    "type": "actionType",      // 2. Тип блока процесса
    "title": "Название шага",  // 3. Отображаемое название блока (необяз.)
    "args": {                  // 4. Параметры блока
        // ...
    },
    "pins": {                  // 5. Выходные связи блока
        "next": "action2"
        // ..
    }
}
  1. name - имя блока процесса. Другие блоки будут ссылаться на этот блок по этому имени
  2. type - тип блока процесса. Различные типы блоков предоставляют различный функционал. Перечень типов блоков представлен в разделе Справка: процессы. Один процесс может содержать несколько блоков одного типа.
  3. title - название блока, отображаемое пользователю при просмотре лога процессов
  4. args - параметры блока. В зависимости от типа блока у блока могут быть различные параметры
  5. pins - выходные связи блока. Указывается имя выхода и имя блока, с которым он связан. Перечень выходов блока зависит от типа блока

После запуска процесса каждый файл проходит блоки процесса независимо от других. Когда файл достигает блока, выход из которого не задан, обработка завершается для файла успешно. Чтобы обозначить, что обработка файла завершилась с ошибкой, необходимо использовать специальное имя выходного блока @fail. Если требуется обозначить, что пользователь отменил операцию, используйте специальное имя блока @cancel

WARNING

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

Рассмотрим следующий пример: необходимо загрузить файл на FTP и в случае ошибки загрузки сделать еще одну попытку через заданный промежуток времени.

Для описания этого процесса нам понадобится два блока:

  • блок загрузки (тип upload)
  • блок задержки (тип delay)

Блок загрузки содержит четыре выхода:

  • успешная загрузка (next)
  • файл не подходит к заданному FTP-соединению (reject). Например, не хватает превью файла, а автоматическая его генерация выключена
  • ошибка во время загрузки (fail)
  • отмена пользователем (cancel). Выполнится, если данные для входа не сохранены и пользователь отказался их вводить

Блок задержки содержит только один выход (next), в который попадает файл после заданного промежутка времени

В коде этот процесс будет описан следующим образом:

{
  "title": "Мой тестовый процесс",
  "icon": null,
  "assetTypes": [
    "photo",
    "illustration",
    "vector",
    "video"
  ],
  "actions": [
    {
      "name": "upload1",
      "type": "upload",
      "args": {
        "connection": "my-ftp"       // Имя FTP-соединения
      },
      "pins": {
        "fail": "delay2",
        "cancel": "@cancel",
        "next": null,
        "reject": "@fail"
      }
    },
    {
      "name": "delay2",
      "type": "delay",
      "args": {
         "delay": 60                 // Задержка в секундах
      },
      "pins": {
        "next": "upload1"
      }
    }
]
}

WARNING

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

Для того, чтобы посмотреть, как выглядит описанный Вами процесс Вы можете использовать кнопку Посмотреть структуру процесса в расширенных настройках сабмита (кнопка отображается только для плагинов в режиме DEV)

# Пример типичного процесса сабмита

Общая идея процесса представлена была в первом разделе. Теперь посмотрим, как ее можно воплотить в жизнь

Схема типичного процесса показана на рисунке ниже:

Рассмотрим по шагам, как описать код этого процесса

TIP

Полный пример плагина вы можете скачать по ссылке, приведенной в разделе Шаблон плагина

# 1. Проверка, соответвует ли файл требованиям стока

Для этого используется блок с типом metadataFilter

{
    "name": "checkMetadata",
    "type": "metadataFilter",
    "args": {
        "filter": [
            "{len(meta.categories) > 0}",
            "{len(meta.keywords) >= 10}"
        ],
        "failMessage": [
            "Категория не задана",
            "Требуется не меньше 10 ключевых слов"
        ]
    },
    "pins": {
        "next": "auth",
        "fail": "@fail"
    }
},
// ...

Блок пропускает файлы дальше (выход next), если они удовлятворяют всем условиям в списке filter. Иначе срабатывает выход fail. Для каждого из условий можно задать свой текст ошибки. Для этого добавьте столько сообщений в параметр failMessage, сколько у Вас условий. Первому условию соответвует первый текст ошибки, второму - второй и так далее.

TIP

Совет: используйте фильтр поиска в программе, чтобы правильно составить условия проверки

Подробнее про блоки с типом metadataFilter можно прочитать здесь

# 2. Авторизуемся на стоке

Для авторизации используется блок с типом browserAuth

// ...
{
    "name": "auth",
    "type": "browserAuth",
    "args": {
        "targetPage": "https://example.com/contributor-page",
        "destination": "my-destination"
    },
    "pins": {
        "cancel": "@cancel",
        "next": "uploadCheck"
    }
},
// ...

Принцип его работы следующий. При активации этого блока в встроенном браузере открывается страница targetPage. Это должна быть некоторая внутренная страница личного кабинета автора. Поскольку изначально пользователь не авторизован, сайт микростока перенаправляет браузер на страницу логина (ее можно переопределить через аргумент loginPage). Как только пользователь авторизуется и попадет на страницу targetPage (можно задать целевую ссылку маской с помощью targetPageMask) блок авторизации пропускает дальше (выход next).

Если пользователь передумал и отказался выполнять вход, то управление передается на выход cancel

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

Подробнее про блоки с типом browserAuth можно прочитать здесь

# 3. Проверям по статусу, не загружен ли уже

Для этого используется блок с типом uploadCheck

// ...
{
    "name": "uploadCheck",
    "type": "uploadCheck",
    "args": {
        "destination": "my-destination"
    },
    "pins": {
        "next": "upload",
        "skip": "batch"
    }
},
// ...

Блок проверяет, есть ли у файла статус "Загрузка" микростока destination. Если статус есть, то файл пропускается (выход skip), если нет - то автивируется выход next.

Подробнее про блоки с типом uploadCheck можно прочитать здесь

# 4. Загрузка и повтор в случае ошибки

Загрузка выполняется с помощью блока upload. Для повтора в случае ошибки загрузки пригодятся два блока attempt и delay

// ...
{
    "name": "upload",
    "type": "upload",
    "args": {
        "connection": "my-ftp"
    },
    "pins": {
        "fail": "uploadAttempt",
        "cancel": "@cancel",
        "next": "afterUploadDelay",
        "reject": "@fail"
    }
},
{
    "name": "uploadAttempt",
    "type": "attempt",
    "args": {
        "num": 3
    },
    "pins": {
        "fail": "@fail",
        "next": "uploadDelay"
    }
},
{
    "name": "uploadDelay",
    "type": "delay",
    "args": {
        "delay": 300
    },
    "pins": {
        "next": "upload"
    }
},
// ...

Блок upload позволяет использовать заданное FTP-подключение (параметр connection) для загрузки на микросток. Если загрузка успешна, активируется выход next. Если файл не подходит к подключению, то срабатывает reject. Если пользователь отменил ввод данных для входа - cancel. Если произошла ошибка загрузки, - выход fail.

В данном случае для реализации повтора в случае ошибки блок upload связан с блоком под названием uploadAttempt с типом attempt. Этот блок позволяет выполнять заданное действие ограниченное количество раз. Если файл зашел в блок не больше num раз, то он выходит через next, иначе - через выход fail.

Блок uploadDelay с типом delay нужен, чтобы выждать время перед следующей попыткой загрузки. Задержка в секундах задается в аргументе delay. После прошествия заданного времени срабатывает выход next.

В разделе справки по процессам дана подробная информация про блоки upload, attempt и delay,

# 5. Ждем, чтобы файл успел появиться на стоке

Зачастую, после загрузки файла на FTP он появляется в интерфейсе микростока не сразу, а по прошествии заданного количества времени. Для этого снова пригодится блок с типом delay

// ...
{
    "name": "afterUploadDelay",
    "type": "delay",
    "args": {
        "delay": 90
    },
    "pins": {
        "next": "batch"
    }
},
// ...

Подробнее про блоки с типом delay можно прочитать здесь

# 6. Дожидаемся загрузки других файлов

Для того, чтобы сабмит выполнялся эффективнее, разумно накопить достаточное количество файлов, перед тем как его вызвать. Для этого используется блок batch

// ...
{
    "name": "batch",
    "type": "batch",
    "args": {
        "num": 5
    },
    "pins": {
        "next": "submit"
    }
},
// ...

В аргументе num указывается, сколько файлов нужно на этом шаге накопить, что пройти дальше (выход next)

Подробнее про блоки с типом batch можно прочитать здесь

# 7. Выполнение сабмита на сайте стока

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

//...
{
    "name": "submit",
    "type": "browserSubmit",
    "args": {
        "destination": "my-destination",
        "connection": "my-ftp",
        "targetPage": "https://example.com/contributor-page",
        "submitCode": { "@textfile": "my-submit-code.js" },
        "dictionaries": { "@jsonfile": "my-submit-dict.json" }
    },
    "pins": {
        "fail": "@fail",
        "notfound": "awaitAttempt",
        "unauthorized": "auth"
    }
},
//...

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

В аргументах указывается:

  • destination - микросток, для которого реализуется сабмит (пользователь должен авторизоваться через блок с типом browserAuth с этим же значением микростока)
  • connection - FTP-соединение, с помощью которого были загружены файлы. Не обязательный аргумент.
  • targetPage - страница, которая будет открыта в скрытом браузере
  • submitCode - javascript-код, который должен быть выполнен. В данном случае код подгружается из файла с помощью специальной директивы { "@textfile": "my-submit-code.js" }. Подробнее о том, как лучше организовать код, рассказано в разделе Код сабмита (Javascript)
  • dictionaries - доп. словарь с данными, который будет передан для использования в коде submitCode. Необязательный параметр. Может использоваться, например, для задания привязки категорий IMS Studio к категориям микростока, чтобы не загромождать основной код, или для передачи локализованных строк при создании мультиязычного плагина (см. раздел Интернационализация). В данном случае словарь подгружается из файла с помощью специальной директивы { "@jsonfile": "my-submit-dict.json" }

У блока browserSubmit четыре выхода. По умолчанию, файлы прошедшие блок, которым не был присвоен никакой другой статус, попадают на выход notfound. В коде submitCode Вы можете перенаправить файлы на остальные три: next - успешный сабмит, fail - ошибка сабмита, unauthorized - авторизация не пройдена (или сбилась).

По умолчанию файлам, которые прошли через выход next, присваивается статус Сабмит (submit). Вы можете переопределить это поведение через аргумент marker

Подробнее про блоки с типом browserSubmit можно прочитать здесь

# 8. Файлов не оказалось на стоке - делаем несколько попыток ожидания

Логика повтора в случае, когда файл не был найден на микростоке, реализуется аналогично, как повторяется загрузка в случае ошибки:

// ...
{
    "name": "awaitAttempt",
    "type": "attempt",
    "args": {
        "num": 5,
        "failMessage": "Файл не найден"
    },
    "pins": {
        "next": "awaitDelay",
        "fail": "@fail"
    }
}, 
{
    "name": "awaitDelay",
    "type": "delay",
    "args": {
        "delay": 300
    },
    "pins": {
            "next": "submit"
    }
}

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

# Код сабмита (Javascript)

Код встраивается на страницу микростока с помощью блока процесса с типом browserSubmit и выполняется после перехода на целевую страницу (см. предыдущий раздел)

Как было сказано ранее код должен выполнить следующее:

  1. найти загруженные файлы на микростоке
  2. загрузить на микросток связанные релизы
  3. сохранить метаданные файлов на микростоке
  4. нажать на кнопку сабмита

Типичная структура кода выглядит следующим образом (читать с конца):

// Получить категории из связанного словаря
const CATS = submitContext.dictionaries.categories

// Проверить авторизованы ли
async function checkAuth() {
  // [...]
  return true;
}

// Получить список незасабмиченных файлов
async function getPendingFiles() {
  // [...] 
  return []
}

// Поиск загруженных файлы на микростоке
async function findAssets(assets) {
   
    // Получаем список незасабмиченных файлов
    const pendingFiles = await getPendingFiles();
    
    // Ищем сопоставление
    const foundPairs = new Map();
    for (const pendingFile of pendingFiles){
        for (const asset of assets){
            if (foundPairs.has(asset)){
               continue; // Сопоставление для этого файла уже найдено
            }
            
            // Проверям, что имена файлов без расширения совпадают
            if (pendingFile.basename === asset.uploadedBasename){
                foundPairs.set(asset, pendingFile)
            }
        }
    }
    
    // Дополнительно проверить список загруженных с ошибкой, если возможно
    // [...]
   
    // Возвращаем те файлы, которые были найдены
    return [...foundPairs.entries()].map(([asset, stockFile]) => {
        return {
            asset,
            stockFile
        }
    })
}

// Найти уже загруженный релиз
async function findReleaseOnStock(releaseAsset) { 
    // [...] 
    return null
}

const ReleasesMap = new Map(); // Кэш релизов
async function uploadReleases(foundAssets) {
    for (const { asset } of foundAssets){
        // Проверям, что к файлу прикреплены релизы
        if (asset.metadata.releases && asset.metadata.releases.length > 0){
            for (const releaseLink of asset.metadata.releases){
                if (ReleasesMap.has(releaseLink.assetId)){
                    continue;
                }
                // Читаем информацию о релизе
                const releaseAsset = await window.imshost.loadAsset(releaseLink.assetId);
                
                // Ищем среди уже загруженных
                const stockReleaseId = await findReleaseOnStock(releaseAsset);
                
                if (stockReleaseId){
                    ReleasesMap.set(releaseLink.assetId, stockReleaseId);
                }
                else {
                    // Получить файл релиза
                    const releaseMainFile = releaseAsset.mainFile;
                    
                    // Прочитать содержимого файла
                    const blob = await releaseMainFile.getBlob();
                    
                    // Загрузить релиз
                    // [...]
                    // ReleasesMap.set(releaseLink.assetId, uploadedId)
                }                
            }
        }
    }
}

// Сохраняем метаданные файлов
async function saveAssets(foundAssets){

    const success = [];
    for (const foundAsset of foundAssets){
       // foundAsset.asset.metadata - метаданные файла
       // [...]
       success.push(foundAsset)
    }

    return success
}

// Выполняем сабмит
async function submitAssets (savedAssets) {
    if (savedAssets.length === 0) return [];

    const success = [];
    for (const savedAsset of savedAssets){
       // [...]
       success.push(savedAsset)
    }

    return success
}

async function processAssets (assets) {
    if (assets.length === 0){
        return;
    }
    
    // Ищем загруженный файлы
    const foundAssets = await findAssets(assets)
    if (foundAssets.length === 0){
        return
    }

    // Загружаем релизы
    await uploadReleases(foundAssets);

    // Сохраняем метаданные 
    const savedAssets = await saveAssets(foundAssets)

    // Выполняем сабмит
    let doneAssets;
    if (submitContext.settings.clickSubmit){
        doneAssets = await submitAssets(savedAssets);
    }
    else doneAssets = savedAssets

    // Отмечаем, что выполнено
    for (const doneAsset of doneAssets){
        const mid = doneAsset.stockFile.id 
        doneAsset.asset.markDone({
            mid // Сохраняем в статусе идентификатор микростока
        });
    }
}

const auth = await checkAuth();
if (!auth){
    // Помечаем все файлы, что пользователь не авторизован
    for (const asset of assets){
        asset.markUnauthorized();
    }
}
else {
    // Выполняем обработку
    await processAssets(assets);
}

На вход подается массив assets - отправляемые файлы.

При успешном сабмите Вы должны вызвать метод asset.markDone({ mid }) и передать в него присвоенный микростоком идентификатор (пригодится в дальнейшем для отслеживания состояния приемки и получения статистики продаж).

Если при обработке файла возникла ошибка, Вы можете отметить его ошибкой с помощью метода asset.markFailed(message), передав в этот метод сообщение об ошибке. Кроме этого, если в процессе работы будет выкинуто необработанное исключение, все файлы, которые еще не были отмечены другими статусами, получат соответвующий статус ошибки.

Если Вы хотите пометить, что файл не найден на микростоке, используйте метод asset.markNotFound(). Тот же самый эффект будет, если файл не получит никакой другой статус, когда код будет уже полностью исполнен.

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

Для того, чтобы получить сохраненные в файле метаданные, обратитесь к свойству asset.metadata. Сюда попадут в том числе и доп. свойства, характерные для этого микростока.

Категории будут заданы в виде массива строк asset.metadata.categories - идентификаторов категорий IMS Studio. Вам потребуется создать словарь для сопоставления этих идентификаторов и категорий на микростоке. Для того, чтобы узнать перечень всех возможных идентификаторов обратитесь к разделу Справка: словари

Релизы будут заданы в виде массива asset.metadata.releases в следующем формате:

{
    // Идентификатор, по которму можно получить все прочие данные
    assetId: "003ded3a-d431-4118-9be3-772122e17d2d:1",
    // Отображаемое имя файла
    name: "Release.jpg",
    title: "Release.jpg"
}

Получить остальные данные по релизу можно с помошью вызова await window.imshost.loadAsset(releaseLink.assetId)

Кроме этого, в код передается объект submitContext, который, в частности, содержит поле settings - сюда попадают настройки сабмита, которые задаются при отправке.

Более подробную информацию про assets и submitContext см. в разделе Справка: контекст сабмита

Для выполнения действий на странице Вам понадобиться отправлять запросы на сервер микростока. Пример сниппетов для выполнения таких запросов здесь и здесь

# Отладка сабмита

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

В этом режиме блоки типа browserSubmit будут выполняться не в скрытом браузере, а в новом окне со включенными инструментами разработчика браузера. Для того, чтобы поставить первую точку останова, используйте специальное ключевое слово debugger.

Иногда так случается, что инструменты разработчика открываются позже того момента, когда выполнится debugger. В результате этого точка остановки пропускается. Чтобы это предотвратить, Вы на момент отладки можете добавить дополнительную задержку:

await new Promise(res => setTimeout(res, 3000)) // трехсекундная пауза
debugger;                                       // вызов точки останова

WARNING

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

Кроме этого, помочь в отладке может логирование. По умолчанию в логи файлов попадают все переходы файлов из одного блока процесса в другой. В коде сабмита Вы также можете записывать в лог файлов дополнительные сообщения. С помощью функции asset.log(message) Вы можете записать обычное сообщения, а с помощью asset.warn(message) - предупреждение.

# Шаблон плагина

Для быстрого старта Вы можете скачать готовый шаблон плагина сабмита по ссылке

После скачивания измените все названия микростоков, FTP-соединений и сабмитов на нужное Вам название микростока. Хотя в примере они названы по разному (чтобы было понятно, что куда вписывается), лучше называть их одинаково, чтобы в дальнейшем не запутаться

# Полезные рецепты

# Сниппет для выполнения запросов, выдающих JSON

// Параметры:
//   method - HTTP-метод запроса, например, GET или POST
//   endpoint - ссылка, куда отправляется запрос
//   params - GET-параметры запроса
//   data - отправляемые данные для POST-запросов. Тип: объект формата ключ-значение
// Возвращает объект
const callApi = async (method, endpoint, params = undefined, data = undefined) => {
    const params_str = params ? new URLSearchParams(params).toString() : '';
    const headers = {
        Accept: 'application/json',
         "Content-Type": 'application/json'
    };
    let body = undefined;
    if (data){
        body = JSON.stringify(data);
    }
    const response = await fetch(endpoint + (params_str ? '?' + params_str : ''), {
        method,
        headers,
        body
     })
    const obj = await response.json()
    // Изменить в зависимости от того, как микросток выдает ошибки
    if (obj.error) throw new Error(obj.error); 
    return obj;
}

# Сниппет для выполнения запросов, выдающих HTML

// Параметры:
//   method - HTTP-метод запроса, например, GET или POST
//   endpoint - ссылка, куда отправляется запрос
//   params - GET-параметры запроса
//   data - отправляемые данные для POST-запросов. Тип: FormData
// Возвращает DOM-документ
async function callPage(method, endpoint, params = undefined, data = undefined) {
    const params_str = params ? new URLSearchParams(params).toString() : '';
    const response = await fetch(endpoint + (params_str ? '?' + params_str : ''), {
        method,
        body: data
     })
    const html = await response.text();
    const parser = new DOMParser();
    return parser.parseFromString(html, 'text/html');
}

# Альтернативная схема авторизации

Если простая схема авторизации, описанная в разделе Пример типичного процесса сабмита не подходит, можно применить более сложную:

Блок checkAuth выполняет код проверки авторизации, используя тип блока browserSubmit. Если пользователь авторизован, то файлы переходят на следующий блок через связь next. В этом случае файлам не нужно назначать никакого статуса сабмита, поэтому аргумент marker блока равен null.

Если же оказалось, что пользователь не авторизован, то срабатывает ветка unauthorized и управление переходит в блок auth. Здесь важно, у блока стоит true в аргументе forceLogin, а также задана ссылка для входа (аргумент loginPage). Это позволяет избежать зацикливания. Если пользователь авторизуется, то управление снова переходит на блок checkAuth, чтобы перепроверить, что авторизация успешна.

// ...
{
    "name": "checkAuth",
    "type": "browserSubmit",
    "args": {
        "targetPage": "https://example.com/contributor-page",
        "destination": "my-destination",
        "submitCode": { "@textfile": "my-check-auth-code.js" },
        "marker": null // Не назначаем никакого маркера после прохождения блока
    },
    "pins":{
        "fail": "@fail",
        "notfound": "@fail",
        "unauthorized": "auth",
        "next": "checkUpload" // Подставьте название шага после успешной авторизации
    }
},
{
    "name": "auth",
    "type": "browserAuth",
    "args": {
        "targetPage": "https://example.com/contributor-page",
        "loginPage": "https://example.com/login-page",
        "destination": "my-destination",
        "forceLogin": true  // Сразу открываем страницу логина
    },
    "pins": {
        "cancel": "@cancel",
        "next": "checkAuth"
    }
},
// ...