#
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.
The code examples later will assume that sessionToken
is assigned your token's value.
#
Step 2: Creating an Upload Session
We'll use the POST /upload/begin
endpoint to create an upload session.
Make sure that you don't have an active session by making a GET request to /upload
. If you have an active session and
want to abandon it, follow the steps here.
#
Request
Let's start by initializing our Group and Manga IDs.
We'll use the Official "Test" Manga and group "test" for the testing of our application.
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
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.
We'll use the Official "Test" Manga and group "test" for the testing of our application.
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
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.
Make sure your files are compliant with the limits stated here, except for the file upload per request, since we'll be managing that here.
Before we start, let's get an overview of the Chapter Upload flow.
- We create a new Upload Session (if there already exists one, we abandon it and create it anew)
- We upload the images to the server
- We save the correlating IDs to the images we uploaded from the server response. We're also wary of any errors for any images
- 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.
For efficiency, the following example code is opinionated. Please note that the only goal is to upload the images to the server, and correspond each filename with its UUID.
#
Request
The form name for each image must be different. Do not name all the forms as "file" because this will cause the server to assume that only one image was uploaded.
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.
I hope you've made sure the files are zeropaded!
#
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
#
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}.`);
}