# Chapter upload code samples

For the purposes of automation, time-saving, or even synchronization, you may want to upload chapters via the API directly. Be sure to follow the guide profusely to avoid any issues in the process.

What you will need:

  • A Mangadex account
  • The group IDs of the groups that worked on the project (unless it's released under No Group by an individual)
  • The manga ID whereunder the chapter is uploaded
  • The images to upload
  • Comprehension and compliance with the upload guidelines

# Step 1: Login

Make sure you have an active session by logging in or refreshing your token. See Authentication.

# Step 2: Creating an Upload Session

We'll use the POST /upload/begin endpoint to create an upload session.

# Request

Let's start by initializing our Group and Manga IDs.

group_ids = ["18dadd0b-cbce-41c4-a8a9-5e653780b9ff"]
manga_id = "f9c33607-9180-4ba6-b85c-e4b5faee7192"

We then create an Upload Session. If another Upload Session is found, make sure to abandon it by following the steps here.

import requests

base_url = "https://api.mangadex.org"

r = requests.post(
    f"{base_url}/upload/begin",
    headers={
        "Authorization": f"Bearer {session_token}"
    },
    json={"groups": group_ids, "manga": manga_id},
)

if r.ok:
    session_id = r.json()["data"]["id"]
    print(f"Created a new Upload Session with ID: {session_id}")
else:
    print("Another session found, please abandon it before creating a new one.")

Let's start by initializing our Group and Manga IDs, as well as the folder path wherein the images are located.

const groupIDs = ['18dadd0b-cbce-41c4-a8a9-5e653780b9ff'];
const mangaID = 'f9c33607-9180-4ba6-b85c-e4b5faee7192';

We then create an Upload Session. If another Upload Session is found, make sure to abandon it by following the steps here.

const axios = require('axios');

const baseUrl = 'https://api.mangadex.org';

let sessionID;

try {
    const resp = await axios({
        method: 'POST',
        url: `${baseUrl}/upload/begin`,
        headers: {
            Authorization: `Bearer ${sessionToken}`,
            'Content-Type': 'application/json'
        },
        data: {
            groups: groupIDs,
            manga: mangaID
        }
    });

    sessionID = resp.data.data.id;
    console.log('Session created with ID', sessionID);
} catch (err) {
    console.log('Another session found, please abandon it before creating a new one.');
}

# Step 3: Upload images to the Upload Session

We have the Upload Session created, with its ID stored, so we are finally ready to start uploading images.

Before we start, let's get an overview of the Chapter Upload flow.

  1. We create a new Upload Session (if there already exists one, we abandon it and create it anew)
  2. We upload the images to the server
  3. We save the correlating IDs to the images we uploaded from the server response. We're also wary of any errors for any images
  4. We commit the Upload Session, providing chapter, volume, title, and language metadata for the chapter, as well as the Page Order

The Page Order is an array of UUIDs which signifies the order in which the pages should be provided when accessing the chapter from the MD@H network. The UUIDs are the image UUIDs we saved from stage 3 which are provided to us by the server as the images are uploaded.

# Request

We'll set the batch size to 5, which means that 5 images are sent per request. You may lower this value if your connection is slow and the request times out.

import os

page_map = []
batch_size = 5
folder_path = "Mangadex/chapter"

for filename in os.listdir(folder_path):
    # omitting non-accepted mimetypes
    if "." not in filename or filename.split(".")[-1].lower() not in ["jpg", "jpeg", "png", "gif"]:
        continue

    page_map.append(
        {
            "filename": filename,
            "extension": filename.split(".")[-1].lower(),
            "path": f"{folder_path}/{filename}",
        }
    )

We will then be reading the files and constructing our form-data request, sending each batch to Mangadex, and then assigning the returned IDs to our succeeded list, while storing each failed page on the failed list.

import requests

base_url = "https://api.mangadex.org"

successful = []
failed = []
batches = [
    page_map[l: l + batch_size]
    for l in range(0, len(page_map), batch_size)
]

for i in range(len(batches)):
    current_batch = batches[i]

    files = [
        (
            f"file{count}",  # the name of the form-data value,
            (
                image["filename"],  # the image's original filename
                open(image["path"], "rb"),  # the image data
                "image/" + image["extension"],  # mime-type
            ),
        )
        for count, image in enumerate(
            current_batch, start=1
        )
    ]

    r = requests.post(
        f"{base_url}/upload/{session_id}",
        headers={
            "Authorization": f"Bearer {session_token}"
        },
        files=files,
    )
    r_json = r.json()

    if r.ok:
        data = r_json["data"]

        for session_file in data:
            successful.append(
                {
                    "id": session_file["id"],
                    "filename": session_file["attributes"]["originalFileName"],
                }
            )

        for image in current_batch:
            if image["filename"] not in [
                page["filename"]
                for page in successful
            ]:
                failed.append(image)

        start = i * batch_size
        end = start + batch_size - 1

        print(
            f"Batch {start}-{end}:",
            "Successful:", len(data), "|",
            "Failed:", len(current_batch) - len(data),
        )
    else:
        print("An error occurred.")
        print(r_json)

We'll set the batch size to 5, which means that 5 images are sent per request. You may lower this value if your connection is slow and the request times out.

const fs = require('fs');

const pageMap = [];
const batchSize = 5;
const folderPath = 'Mangadex/chapter';

fs.readdirSync(folderPath).forEach(filename => {
    if (!filename.includes('.') || !['jpg', 'jpeg', 'png', 'gif'].includes(filename.split('.').at(-1).toLowerCase())) {
        return;
    }

    pageMap.push({
        filename: filename,
        extension: filename.split('.').at(-1).toLowerCase(),
        path: `${folderPath}/${filename}`
    });
});

We will then be reading the files and constructing our form-data request, sending each batch to Mangadex, and then assigning the returned IDs to our succeeded array, while storing each failed page on the failed array.

const axios = require('axios');
const FormData = require('form-data'); // delete this if you're on a browser

const baseUrl = 'https://api.mangadex.org';

const successful = [];
const failed = [];
const batches = [];

for (var i = 0; i < pageMap.length; i += batchSize) {
    batches.push(pageMap.slice(i, i + batchSize));
}

if (batches.length * batchSize < pageMap.length && pageMap.length > batchSize) {
    batches.push(pageMap.slice(batches.length * batchSize));
}

let formData;
let start, end;

for (const i in batches) {
    formData = new FormData();

    batches[i].forEach((page, index) => {
        formData.append(
            `file${index + 1}`,
            fs.readFileSync(page.path),
            page.filename
        );
    });

    try {
        const resp = await axios({
            method: 'POST',
            url: `${baseUrl}/upload/${sessionID}`,
            headers: {
                Authorization: `Bearer ${sessionToken}`,
                'Content-Type': 'multipart/form-data'
            },
            data: formData
        });

        resp.data.data.forEach(sessionFile => {
            successful.push({
                id: sessionFile.id,
                filename: sessionFile.attributes.originalFileName
            })
        });
        batches[i].forEach(page => {
            if (!successful.map(i => i.filename).includes(page.filename)) {
                failed.push(page);
            }
        });
        start = i * batchSize;
        end = start + batchSize - 1;
        console.log(
            `Batch ${start}-${end}:`,
            `Successful: ${resp.data.data.length}`,
            `Failed: ${batches[i].length - resp.data.data.length}`
        );
    } catch (err) {
        console.error('An error occurred');
        console.error(err);
        failed.push(...pageMap.slice(i, i + batchSize));
    }
}

# Step 4: Sorting the Page Order, and committing the Upload Session

Now that our files are uploaded to the server, there's one final step before our chapter is sent to the Upload Queue. We have to provide the server with the Page Order, chapter, title, and volume metadata. Always refer to the site rules when deciding on what data you'll put on each field.

There are an infinitely many ways to sort the Page Order, the way we approached this is by creating a list/array successful that stores the ID of the image and its corresponding filename. We'll sort this list based on the filename, and then extract each ID.

# Request
successful.sort(key=lambda a: a["filename"])

page_order = [page["id"] for page in successful]

chapter_draft = {
    "volume": None,
    "chapter": "5",
    "translatedLanguage": "en",
    "title": "MD Docs Python code example test",
}
r = requests.post(
    f"{base_url}/upload/{session_id}/commit",
    headers={
        "Authorization": f"Bearer {session_token}"
    },
    json={
        "chapterDraft": chapter_draft,
        "pageOrder": page_order,
    },
)

if r.ok:
    print(
        "Upload Session successfully committed, entity ID is:",
        r.json()["data"]["id"],
    )
else:
    print("An error occurred.")
    print(r.json())
successful.sort((a, b) => {
    const nameA = a.filename.toUpperCase();
    const nameB = b.filename.toUpperCase();

    if (nameA < nameB) {
        return -1
    }
    if (nameA > nameB) {
        return 1
    }

    return 0;
});
const pageOrder = successful.map(i => i.id);

const chapterDraft = {
    volume: null,
    chapter: '5',
    translatedLanguage: 'en',
    title: 'MD Docs JavaScript code example test'
};
try {
    const resp = await axios({
        method: 'POST',
        url: `${baseUrl}/upload/${sessionID}/commit`,
        headers: {
            Authorization: `Bearer ${sessionToken}`,
            'Content-Type': 'application/json'
        },
        data: {
            chapterDraft: chapterDraft,
            pageOrder: pageOrder
        }
    });
    console.log('Upload Session successfully committed, entity ID is:', resp.data.data.id);
} catch (err) {
    console.log('An error occurred.');
    console.error(err);
}

# Fallback: Deleting the Upload Session we had created.

If we want to abandon the session, we'll have to let the server know, as only one active upload session is allowed per user.

# I don't know the session ID.

If you don't know the session ID, you'll need to call GET /upload as logged in. Otherwise, if you do, jump straight into abandoning it.

# Request
import requests

base_url = "https://api.mangadex.org"

r = requests.get(
    f"{base_url}/upload",
    headers={
        "Authorization": f"Bearer {session_token}"
    },
)

if r.ok:
    session_id = r.json()["data"]["id"]
    print("Found a session with ID:", session_id)
else:
    print("No active session found.")
const axios = require('axios');

const baseUrl = 'https://api.mangadex.org';

let sessionID;

try {
    const resp = await axios({
        method: 'GET',
        url: `${baseUrl}/upload`,
        headers: {
            Authorization: `Bearer ${sessionToken}`
        }
    });

    sessionID = resp.data.data.id;
    console.log('Found a session with ID:', sessionID);
} catch (err) {
    console.error(err);
    console.log('No active session found.');
}

# I know the session ID.

If you know the session ID (or have obtained it from the previous section), then it's really trivial to abandon the session. Let's suppose our session ID is 0301208d-258a-444a-8ef7-66e433d801b1.

# Request
session_id = "0301208d-258a-444a-8ef7-66e433d801b1"
import requests

base_url = "https://api.mangadex.org"

r = requests.delete(
    f"{base_url}/upload/{session_id}",
    headers={
        "Authorization": f"Bearer {session_token}"
    },
)

if r.ok:
    print(f"Successfully abandoned session {session_id}.")
else:
    print(f"Could not abandon session {session_id}, status code: {r.status_code}")
const sessionID = '0301208d-258a-444a-8ef7-66e433d801b1';
const axios = require('axios');

const baseUrl = 'https://api.mangadex.org';

try {
    const resp = await axios({
        method: 'DELETE',
        url: `${baseUrl}/upload/${sessionID}`,
        headers: {
            Authorization: `Bearer ${sessionToken}`
        }
    });

    console.log(`Successfully abandoned session ${sessionID}.`);
} catch (err) {
    console.log(`Could not abandon session ${sessionID}, status code: ${err?.status}.`);
}