Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for creating mangas and covers #56

Merged
merged 8 commits into from
Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/internal/localizedstring.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/**
* Represents a string, but in different languages.
* Generates properties for each language available
* Generates properties for each language available
* (ie you can index with language codes through localizedString['en'] or localizedString.jp)
*/
class LocalizedString {
Expand All @@ -14,7 +14,7 @@ class LocalizedString {
static locale = 'en';

/**
* @param {Object.<string, string>} stringObject
* @param {Object.<string, string>} stringObject
*/
constructor(stringObject) {
if (!stringObject) {
Expand All @@ -40,6 +40,17 @@ class LocalizedString {
for (let i of this.availableLocales) if (i in this) return this[i];
return null;
}

/**
* Gets an object
* @returns {{[locale: string]: string}}
*/
get data(){
return this.availableLocales.reduce((obj, locale) => {
obj[locale] = this[locale];
return obj;
}, {});
}
}

exports = module.exports = LocalizedString;
14 changes: 12 additions & 2 deletions src/structure/author.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Relationship = require('../internal/relationship.js');
class Author {
/**
* There is no reason to directly create an author object. Use static methods, ie 'get()'.
* @param {Object|String} context Either an API response or Mangadex id
* @param {Object|String} context Either an API response or Mangadex id
*/
constructor(context) {
if (typeof context === 'string') {
Expand Down Expand Up @@ -72,7 +72,7 @@ class Author {
* @property {String[]} [AuthorParameterObject.ids] Max of 100 per request
* @property {Number} [AuthorParameterObject.limit] Not limited by API limits (more than 100). Use Infinity for maximum results (use at your own risk)
* @property {Number} [AuthorParameterObject.offset]
* @property {Object} [AuthorParameterObject.order]
* @property {Object} [AuthorParameterObject.order]
* @property {'asc'|'desc'} [AuthorParameterObject.order.name]
*/

Expand All @@ -89,6 +89,16 @@ class Author {
return Util.apiCastedRequest('/author', Author, searchParameters);
}

/**
* Create a new Author.
* @param {string} [name] The name of the author.
* @param {Object | undefined} [options] Additional arguments to pass to the API.
* @returns {Promise<Author>}
*/
static async create(name, options) {
return new Author(await Util.apiRequest('/author', 'POST', { name, ...options }));
}

/**
* Gets multiple authors
* @param {...String|Author|Relationship<Author>} ids
Expand Down
32 changes: 31 additions & 1 deletion src/structure/cover.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Util = require('../util.js');
class Cover {
/**
* There is no reason to directly create a cover art object. Use static methods, ie 'get()'.
* @param {Object|String} context Either an API response or Mangadex id
* @param {Object|String} context Either an API response or Mangadex id
*/
constructor(context) {
if (typeof context === 'string') {
Expand Down Expand Up @@ -122,6 +122,36 @@ class Cover {
return Util.apiCastedRequest('/cover', Cover, searchParameters);
}

/**
* @ignore
* @typedef {Object} CoverUploadParameterObject
* @property {string|null} [CoverUploadParameterObject.volume] Volume of the cover
* @property {string} [CoverUploadParameterObject.description] Description of the cover
*/

/**
* @ignore
* @typedef {Object} CoverFileObject
* @property {Buffer} CoverFileObject.data
* @property {'jpeg'|'png'|'gif'} [CoverFileObject.type]
* @property {String} CoverFileObject.name
*/

/**
* Creates a new cover.
* @param {string} [mangaId] The id of the manga that the cover is for.
* @param {CoverFileObject} [file] The buffer containing the image data.
* @param {CoverUploadParameterObject | undefined} [options] Additional options for the cover upload.
* @returns {Promise<Cover>}
*/
static async create(mangaId, file, options){
options = options || {};
return new Cover(await Util.apiRequest(`/cover/${mangaId}`, 'POST', Util.createMultipartPayload([file], {
volume: options.volume,
description: options.description
})))
}

/**
* Gets multiple covers
* @param {...String|Cover|Relationship<Cover>} ids
Expand Down
104 changes: 79 additions & 25 deletions src/structure/manga.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const UploadSession = require('../internal/uploadsession.js');
class Manga {
/**
* There is no reason to directly create a manga object. Use static methods, ie 'get()'.
* @param {Object|String} context Either an API response or Mangadex id
* @param {Object|String} context Either an API response or Mangadex id
*/
constructor(context) {
if (typeof context === 'string') {
Expand Down Expand Up @@ -151,31 +151,37 @@ class Manga {
/**
* @ignore
* @typedef {Object} RelatedMangaObject
* @property {Manga[]} RelatedMangaObject.monochrome
* @property {Manga[]} RelatedMangaObject.main_story
* @property {Manga[]} RelatedMangaObject.adapted_from
* @property {Manga[]} RelatedMangaObject.based_on
* @property {Manga[]} RelatedMangaObject.prequel
* @property {Manga[]} RelatedMangaObject.side_story
* @property {Manga[]} RelatedMangaObject.doujinshi
* @property {Manga[]} RelatedMangaObject.same_franchise
* @property {Manga[]} RelatedMangaObject.shared_universe
* @property {Manga[]} RelatedMangaObject.sequel
* @property {Manga[]} RelatedMangaObject.spin_off
* @property {Manga[]} RelatedMangaObject.alternate_story
* @property {Manga[]} RelatedMangaObject.preserialization
* @property {Manga[]} RelatedMangaObject.colored
* @property {Manga[]} RelatedMangaObject.serialization
* @property {Manga[]} RelatedMangaObject.monochrome
* @property {Manga[]} RelatedMangaObject.main_story
* @property {Manga[]} RelatedMangaObject.adapted_from
* @property {Manga[]} RelatedMangaObject.based_on
* @property {Manga[]} RelatedMangaObject.prequel
* @property {Manga[]} RelatedMangaObject.side_story
* @property {Manga[]} RelatedMangaObject.doujinshi
* @property {Manga[]} RelatedMangaObject.same_franchise
* @property {Manga[]} RelatedMangaObject.shared_universe
* @property {Manga[]} RelatedMangaObject.sequel
* @property {Manga[]} RelatedMangaObject.spin_off
* @property {Manga[]} RelatedMangaObject.alternate_story
* @property {Manga[]} RelatedMangaObject.preserialization
* @property {Manga[]} RelatedMangaObject.colored
* @property {Manga[]} RelatedMangaObject.serialization
*/

/**
* @type {RelatedMangaObject}
*/
this.relatedManga = Object.fromEntries([
'monochrome', 'main_story', 'adapted_from', 'based_on', 'prequel',
'side_story', 'doujinshi', 'same_franchise', 'shared_universe', 'sequel',
'monochrome', 'main_story', 'adapted_from', 'based_on', 'prequel',
'side_story', 'doujinshi', 'same_franchise', 'shared_universe', 'sequel',
'spin_off', 'alternate_story', 'preserialization', 'colored', 'serialization'
].map(k => [k, Relationship.convertType('manga', context.data.relationships.filter(r => r.related === k))]));

/**
* The version of this manga (incremented by updating manga data)
* @type {Number}
*/
this.version = context.data.attributes.version ?? 1;
}

/**
Expand Down Expand Up @@ -252,6 +258,25 @@ class Manga {
return Util.apiCastedRequest('/manga', Manga, searchParameters);
}

/**
* Creates a manga.
* @param {LocalizedString | Object} [title] The title of the manga.
* @param {string} [originalLanguage] The original language of the manga.
* @param {'ongoing'|'completed'|'hiatus'|'cancelled'} [status] The status of the manga.
* @param {'safe'|'suggestive'|'erotica'|'pornographic'} [contentRating] The content rating of the manga.
* @param {Object | undefined} [options] Additional options for creating the manga.
* @returns {Promise<Manga>}
*/
static async create(title, originalLanguage, status, contentRating, options){
return new Manga(await Util.apiRequest('/manga', 'POST', {
title: title.data ?? title,
originalLanguage,
status,
contentRating,
...options
}));
}

/**
* Gets multiple manga
* @param {...String|Relationship<Manga>} ids
Expand Down Expand Up @@ -371,7 +396,7 @@ class Manga {
}

/**
* Sets the logged in user's reading status for this manga.
* Sets the logged in user's reading status for this manga.
* Call without arguments to clear the reading status
* @param {String} id
* @param {'reading'|'on_hold'|'plan_to_read'|'dropped'|'re_reading'|'completed'} [status]
Expand Down Expand Up @@ -408,7 +433,7 @@ class Manga {

/**
* Makes the logged in user either follow or unfollow a manga
* @param {String} id
* @param {String} id
* @param {Boolean} [follow=true] True to follow, false to unfollow
* @returns {Promise<void>}
*/
Expand All @@ -420,7 +445,7 @@ class Manga {
/**
* Retrieves the read chapters for multiple manga
* @param {...String|Manga|Relationship<Manga>} ids
* @returns {Promise<Chapter[]>}
* @returns {Promise<Chapter[]>}
*/
static async getReadChapters(...ids) {
if (ids.length === 0) throw new Error('Invalid Argument(s)');
Expand Down Expand Up @@ -461,7 +486,7 @@ class Manga {
* Returns a summary of every chapter for a manga including each of their numbers and volumes they belong to
* https://api.mangadex.org/docs.html#operation/post-manga
* @param {String} id
* @param {...String|String[]} languages
* @param {...String|String[]} languages
* @returns {Promise<Object.<string, AggregateVolume>>}
*/
static async getAggregate(id, ...languages) {
Expand Down Expand Up @@ -501,7 +526,7 @@ class Manga {

/**
* Returns the rating and follow count of a manga
* @param {String} id
* @param {String} id
* @returns {Statistics}
*/
static async getStatistics(id) {
Expand Down Expand Up @@ -564,7 +589,7 @@ class Manga {
}

/**
* Sets the logged in user's reading status for this manga.
* Sets the logged in user's reading status for this manga.
* Call without arguments to clear the reading status
* @param {'reading'|'on_hold'|'plan_to_read'|'dropped'|'re_reading'|'completed'} [status]
* @returns {Promise<Manga>}
Expand Down Expand Up @@ -595,12 +620,41 @@ class Manga {
/**
* Returns a summary of every chapter for this manga including each of their numbers and volumes they belong to
* https://api.mangadex.org/docs.html#operation/post-manga
* @param {...String} languages
* @param {...String} languages
* @returns {Promise<Object.<string, AggregateVolume>>}
*/
getAggregate(...languages) {
return Manga.getAggregate(this.id, ...languages);
}

/**
* Updates a manga's information using the information stored in the model and returns a new Manga.
* @returns {Promise<Manga>}
*/
async update() {
const data = await Util.apiRequest(`manga/${this.id}`, 'PUT', {
title: this.localizedTitle.data,
altTitles: this.localizedAltTitles.map(altTitle => altTitle.data),
description: this.localizedDescription.data,
authors: this.authors.map(author => author.id),
artists: this.artists.map(artist => artist.id),
tags: this.tags.map(tag => tag.id),
links: this.links.availableLinks.reduce((prev, key) => {
prev[key] = this.links[key];
return prev;
}, {}),
originalLanguage: this.originalLanguage,
lastVolume: this.lastVolume,
lastChapter: this.lastChapter,
status: this.status,
publicationDemographic: this.publicationDemographic,
year: this.year,
contentRating: this.contentRating,
primaryCover: this.mainCover.id,
version: this.version
});
return new Manga(data)
}
}

exports = module.exports = Manga;
14 changes: 11 additions & 3 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ async function apiParameterRequest(baseEndpoint, parameterObject) {
exports.apiParameterRequest = apiParameterRequest;

/**
* Same as apiParameterRequest, but optimized for search requests.
* Same as apiParameterRequest, but optimized for search requests.
* Allows for larger searches (more than the limit max, even to Infinity) through mutliple requests, and
* this function always returns an array instead of the normal JSON object.
* @param {String} baseEndpoint Endpoint with no parameters
Expand Down Expand Up @@ -199,7 +199,7 @@ exports.apiCastedRequest = apiCastedRequest;

/**
* Retrieves an unlimted amount of an object via a search function and id array
* @param {Function} searchFunction
* @param {Function} searchFunction
* @param {String[]|String[][]} ids
* @param {Number} [limit=100]
* @param {String} [searchProperty='ids']
Expand All @@ -222,10 +222,18 @@ exports.getMultipleIds = getMultipleIds;
/**
* Returns a buffer to be sent with a multipart POST request
* @param {Object[]} files
* @param {{[key: string]: string}} [extra] Additional key-value pairs
* @returns {Buffer}
*/
function createMultipartPayload(files) {
function createMultipartPayload(files, extra) {
let dataArray = [];
if (extra) {
Object.entries(extra).forEach(([key, value]) => {
dataArray.push(
`--${MULTIPART_BOUNDARY}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`
);
});
}
files.forEach((file, i) => {
dataArray.push(
`--${MULTIPART_BOUNDARY}\r\nContent-Disposition: form-data; name="file${i}"; filename="${file.name}"\r\nContent-Type: ${file.type}\r\n\r\n`
Expand Down
Loading