├── 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 |
--------------------------------------------------------------------------------