Skip to content

Commit

Permalink
Add support for creating mangas and covers (#56)
Browse files Browse the repository at this point in the history
* Add support for creating mangas and covers

* Update types

* Add support for author creation

* Modify manga create to work with LocalizedStrings.

* Add support for not using localized strings

* Add support for updating.

* Add back constructor method

* Revert _parse and update types

Co-authored-by: md-y <midymyth@gmail.com>
  • Loading branch information
PythonCoderAS and md-y authored Jan 26, 2022
1 parent c007772 commit a6df1ef
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 33 deletions.
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

0 comments on commit a6df1ef

Please sign in to comment.