├── Procfile ├── index.html ├── .gitignore ├── package.json ├── LICENSE ├── MAL.js ├── index.js └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

Kuristina MAL Assistant

11 |

Refer to https://github.com/TimboKZ/kuristina for more info.

12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # JetBrains IDE files 2 | .idea 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kuristina", 3 | "version": "1.0.0", 4 | "description": "An API to fetch anime lists using MyAnimeList account names.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/TimboKZ/kuristina.git" 13 | }, 14 | "engines": { 15 | "node": "6.9.1" 16 | }, 17 | "author": { 18 | "name": "Timur Kuzhagaliyev", 19 | "email": "tim.kuzh@gmail.com", 20 | "url": "https://foxypanda.me/" 21 | }, 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/TimboKZ/kuristina/issues" 25 | }, 26 | "homepage": "https://github.com/TimboKZ/kuristina#readme", 27 | "dependencies": { 28 | "cheerio": "^0.22.0", 29 | "express": "^4.14.0", 30 | "path": "^0.12.7", 31 | "request": "^2.75.0", 32 | "xml2js": "^0.4.17" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Tim K. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MAL.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file File responsible for all interactions with MyAnimeList website. 3 | * @author Timur Kuzhagaliyev 4 | * @copyright 2016 5 | * @license MIT 6 | * @version 0.0.4 7 | */ 8 | 9 | const request = require('request'); 10 | 11 | /** 12 | * @class Class used to fetch anime list contents for different users. 13 | * @since 0.0.1 14 | */ 15 | class MAL { 16 | /** 17 | * Get anime list as XML using `malappinfo.php`. 18 | * @param {string} list 19 | * @param {string} username 20 | * @param callback 21 | * @since 0.0.3 Now returns a specific error if user was not found 22 | * @since 0.0.1 23 | */ 24 | static getListXml(list, username, callback) { 25 | let url = 'http://myanimelist.net/malappinfo.php?u=' + username + '&status=all&type=' + list; 26 | console.log('Fetching ' + url); 27 | request(url, function(error, response, content) { 28 | if(content.indexOf('Invalid username') !== -1) { 29 | return callback('User not found'); 30 | } 31 | callback(error, content); 32 | }); 33 | } 34 | /** 35 | * Get anime list as XML using `malappinfo.php`. 36 | * @param {string} list 37 | * @param {string} username 38 | * @param callback 39 | * @since 0.0.4 Now displays empty string as null 40 | * @since 0.0.2 41 | */ 42 | static getListJson(list, username, callback) { 43 | this.getListXml(list, username, function(error, content) { 44 | if(error) { 45 | return callback(error); 46 | } 47 | const parseString = require('xml2js').parseString; 48 | parseString(content, { explicitArray: false, emptyTag: null }, function(error, json) { 49 | if(error) { 50 | return callback(error); 51 | } 52 | callback(error, JSON.stringify(json)); 53 | }); 54 | }); 55 | } 56 | 57 | /** 58 | * Get anime list as JSON from the `data-items` attribute on `table.list-table` on the page of the anime list. 59 | * This list contains some additional information not available through `getListJson()`, but it is also missing 60 | * some vital information like account info. 61 | * @param {string} list 62 | * @param {string} username 63 | * @param callback 64 | * @since 0.0.2 Renamed from `getListJson` to `getScrapedListJson` 65 | * @since 0.0.1 66 | */ 67 | static getScrapedListJson(list, username, callback) { 68 | let url = 'https://myanimelist.net/' + list + 'list/' + username; 69 | console.log('Fetching ' + url); 70 | request(url, function(error, response, html) { 71 | if(error) { 72 | return callback(error); 73 | } 74 | const cheerio = require('cheerio'); 75 | let $ = cheerio.load(html); 76 | let content = $('table.list-table').first().attr('data-items'); 77 | if(!content || content.length < 1) { 78 | return callback('User not found'); 79 | } 80 | callback(error, content); 81 | }); 82 | } 83 | } 84 | 85 | module.exports = MAL; 86 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Main Kuristina script responsible for running the server and routing all requests. 3 | * @author Timur Kuzhagaliyev 4 | * @copyright 2016 5 | * @license MIT 6 | * @version 0.0.3 7 | */ 8 | 9 | const express = require('express'); 10 | const path = require('path'); 11 | let MAL = require('./MAL'); 12 | 13 | /** 14 | * Port the server will be listening in. Unless specified by an environment variable (e.g. on a Heroku server), 15 | * 3000 is used. 16 | * @since 0.0.1 17 | */ 18 | const PORT = process.env.PORT || 3000; 19 | 20 | /** 21 | * List types supported by MyAnimeList. 22 | * Since 0.0.2 23 | */ 24 | const LIST_TYPES = ['anime', 'manga']; 25 | 26 | /** 27 | * Response formats supported by Kuristina. 28 | * Since 0.0.2 29 | */ 30 | const RESPONSE_FORMATS = ['xml', 'json']; 31 | 32 | /** 33 | * Create an instance of Express and enable CORS 34 | * @since 0.0.3 Enabled CORS 35 | * @since 0.0.1 36 | */ 37 | let app = express(); 38 | app.use(function(req, res, next) { 39 | res.header("Access-Control-Allow-Origin", "*"); 40 | res.header("Access-Control-Allow-Headers", "X-Requested-With"); 41 | next(); 42 | }); 43 | 44 | /** 45 | * Show a nice welcome message to all request without the username specified, and point them at the GitHub page. 46 | * @since 0.0.2 Now uses an absolute path 47 | * @since 0.0.1 48 | */ 49 | app.get('/', function (req, res) { 50 | res.sendFile(path.join(__dirname, 'index.html')); 51 | }); 52 | 53 | /** 54 | * Extract the list type (anime/manga), the username and the password and verify that all of them are valid. 55 | * If not, send 403 error, otherwise call an appropriate method from MAL class. 56 | * @since 0.0.2 57 | */ 58 | app.get('/:list/:username.:format', function (req, res) { 59 | 60 | let list = req.params.list.toLowerCase(); 61 | let username = req.params.username.toLowerCase(); 62 | let format = req.params.format.toLowerCase(); 63 | 64 | if(LIST_TYPES.indexOf(list) === -1) { 65 | return res.send(400, 'Invalid list type specified! Supported list types: ' + LIST_TYPES.toString()); 66 | } 67 | if(!/^[A-Za-z0-9-_]+$/.test(username)) { 68 | return res.send(400,'Invalid username specified! Usernames must only contain letters, nubmer, dashes and underscores.'); 69 | } 70 | if(RESPONSE_FORMATS.indexOf(format) === -1) { 71 | return res.send(400, format + 'Invalid format specified! Supported formats: ' + RESPONSE_FORMATS.toString()); 72 | } 73 | 74 | let generateResponse = function generateResponse(error, content) { 75 | if(error) { 76 | if(error === 'User not found') { 77 | return res.send(404, 'No user corresponding to the specified username was found.'); 78 | } 79 | return res.send(500, 'An error occurred while accessing MAL: ' + error); 80 | } 81 | res.header('Content-Type', 'application/' + format); 82 | return res.send(content); 83 | }; 84 | 85 | if(format === 'xml') { 86 | return MAL.getListXml(list, req.params.username, generateResponse); 87 | } 88 | if(format === 'json') { 89 | return MAL.getListJson(list, req.params.username, generateResponse); 90 | } 91 | 92 | }); 93 | 94 | app.listen(PORT, function () { 95 | console.log('Kuristina is listening on port ' + PORT + '!'); 96 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kuristina 2 | 3 | > **IMPORTANT:** Kuristina has been **deprecated** since the MAL API blackout in May 2018 took down one of the critical MAL endpoints. Please use [Jikan](https://jikan.docs.apiary.io/#reference/0/user) or a similar project. 4 | 5 | Kuristina is here to assist you in fetching list contents for both manga and anime lists from MyAnimeList. This page contains all of the documentation you need to use it, but if you want to find out more about how it was developed refer to [this article](https://foxypanda.me/my-anime-timeline-and-kuristina/). 6 | 7 | # Features 8 | 9 | At the moment, Kuristina only has 2 features: 10 | 11 | * Fetching anime list by username, printing the result in either XML or JSON. 12 | * Fetching manga list by username, printing the result in either XML or JSON. 13 | 14 | This list is most likely to expand over time, so you might wanna check this page once in a while if you're interested. 15 | 16 | > **Note:** Kuristina now has a [Node.js wrapper](https://www.npmjs.com/package/kuristina). 17 | 18 | # Usage 19 | 20 | At the moment Kuristina only accepts GET request of the following format: 21 | 22 | ``` 23 | https://kuristina.herokuapp.com//. 24 | ``` 25 | 26 | Where `` is either `anime` or `manga` to fetch anime or manga list respectively, `` is the name of the user whose list you want to fetch and finally `` is the format in which the list will be returned, either `xml` or `json`. All of the 3 parameters are case-insensitive. Consider the examples below 27 | 28 | ``` 29 | # Fetch Timbo_KZ's manga list in XML 30 | GET https://kuristina.herokuapp.com/manga/Timbo_KZ.xml 31 | 32 | # Fetch Timbo_KZ's anime list in JSON 33 | GET https://kuristina.herokuapp.com/anime/Timbo_KZ.json 34 | ``` 35 | 36 | # Possible errors 37 | 38 | Here are several possible cases which might have to handle in your applcaition: 39 | 40 | * If you access any other URL but the index or the URLs of format specified above, Kuristina will return `404 Not Found` status code. 41 | * If the list type is not supported, username does not appear to be valid (i.e. MAL user names can only contain letters, numbers, dashes and underscores) or format specified is not supported Kuristina will return `400 Bad Request` status code. 42 | * If you request a list and the username does not exist (i.e. MAL cannot find it), `404 Not Found` status code will be returned. 43 | * If any other error occurs during fetching, server will return `500 Internal Server Error` status code. 44 | 45 | # Example responses 46 | 47 | Anime list in XML: 48 | 49 | ```xml 50 | 51 | 52 | 4718042 53 | Timbo_KZ 54 | 57 55 | 125 56 | 1 57 | 1 58 | 31 59 | 43.86 60 | 61 | 62 | 1 63 | Cowboy Bebop 64 | COWBOY BEBOP; Cowboy Bebop 65 | 1 66 | 26 67 | 2 68 | 1998-04-03 69 | 1999-04-24 70 | 71 | https://myanimelist.cdn-dena.com/images/anime/4/19644.jpg 72 | 73 | 0 74 | 26 75 | 2016-02-15 76 | 2016-04-02 77 | 8 78 | 2 79 | 0 80 | 0 81 | 1459548352 82 | 83 | 84 | 85 | ``` 86 | 87 | Anime list in JSON: 88 | 89 | ```json 90 | { 91 | "myanimelist":{ 92 | "myinfo":{ 93 | "user_id":"4718042", 94 | "user_name":"Timbo_KZ", 95 | "user_watching":"57", 96 | "user_completed":"125", 97 | "user_onhold":"1", 98 | "user_dropped":"1", 99 | "user_plantowatch":"31", 100 | "user_days_spent_watching":"43.86" 101 | }, 102 | "anime":[ 103 | { 104 | "series_animedb_id":"1", 105 | "series_title":"Cowboy Bebop", 106 | "series_synonyms":"COWBOY BEBOP; Cowboy Bebop", 107 | "series_type":"1", 108 | "series_episodes":"26", 109 | "series_status":"2", 110 | "series_start":"1998-04-03", 111 | "series_end":"1999-04-24", 112 | "series_image":"https://myanimelist.cdn-dena.com/images/anime/4/19644.jpg", 113 | "my_id":"0", 114 | "my_watched_episodes":"26", 115 | "my_start_date":"2016-02-15", 116 | "my_finish_date":"2016-04-02", 117 | "my_score":"8", 118 | "my_status":"2", 119 | "my_rewatching":"0", 120 | "my_rewatching_ep":"0", 121 | "my_last_updated":"1459548352", 122 | "my_tags":null 123 | } 124 | ] 125 | } 126 | } 127 | ``` 128 | 129 | Manga list in XML: 130 | 131 | ```xml 132 | 133 | 134 | 4718042 135 | Timbo_KZ 136 | 6 137 | 0 138 | 0 139 | 0 140 | 0 141 | 0.99 142 | 143 | 144 | 7776 145 | Toaru Kagaku no Railgun 146 | 147 | To Aru Kagaku no Choudenjihou; A Certain Scientific Railgun 148 | 149 | 1 150 | 0 151 | 0 152 | 1 153 | 2007-05-27 154 | 0000-00-00 155 | 156 | https://myanimelist.cdn-dena.com/images/manga/1/149212.jpg 157 | 158 | 45546470 159 | 79 160 | 0 161 | 2015-09-14 162 | 0000-00-00 163 | 10 164 | 1 165 | 166 | 0 167 | 1444721961 168 | 169 | 170 | 171 | ``` 172 | 173 | Manga list in JSON: 174 | 175 | ```json 176 | { 177 | "myanimelist":{ 178 | "myinfo":{ 179 | "user_id":"4718042", 180 | "user_name":"Timbo_KZ", 181 | "user_reading":"6", 182 | "user_completed":"0", 183 | "user_onhold":"0", 184 | "user_dropped":"0", 185 | "user_plantoread":"0", 186 | "user_days_spent_watching":"0.99" 187 | }, 188 | "manga":[ 189 | { 190 | "series_mangadb_id":"7776", 191 | "series_title":"Toaru Kagaku no Railgun", 192 | "series_synonyms":"To Aru Kagaku no Choudenjihou; A Certain Scientific Railgun", 193 | "series_type":"1", 194 | "series_chapters":"0", 195 | "series_volumes":"0", 196 | "series_status":"1", 197 | "series_start":"2007-05-27", 198 | "series_end":"0000-00-00", 199 | "series_image":"https://myanimelist.cdn-dena.com/images/manga/1/149212.jpg", 200 | "my_id":"45546470", 201 | "my_read_chapters":"79", 202 | "my_read_volumes":"0", 203 | "my_start_date":"2015-09-14", 204 | "my_finish_date":"0000-00-00", 205 | "my_score":"10", 206 | "my_status":"1", 207 | "my_rereadingg":null, 208 | "my_rereading_chap":"0", 209 | "my_last_updated":"1444721961", 210 | "my_tags":null 211 | } 212 | ] 213 | } 214 | } 215 | ``` 216 | --------------------------------------------------------------------------------