Skip to content

Commit

Permalink
Init
Browse files Browse the repository at this point in the history
  • Loading branch information
lubien committed Jun 17, 2016
0 parents commit 9660d8d
Show file tree
Hide file tree
Showing 20 changed files with 323 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .babelrc
@@ -0,0 +1,5 @@
{
"presets": [
"es2015"
]
}
9 changes: 9 additions & 0 deletions .editorconfig
@@ -0,0 +1,9 @@
root = true

[*]
encoding = utf-8
end_of_line = lf
insert_final_newline = true

[*.js]
indent_style = tab
2 changes: 2 additions & 0 deletions .env.sample
@@ -0,0 +1,2 @@
MAL_USER=
MAL_PASS=
4 changes: 4 additions & 0 deletions .gitignore
@@ -0,0 +1,4 @@
node_modules/
dist/
docs/
.env
9 changes: 9 additions & 0 deletions .jsdocrc
@@ -0,0 +1,9 @@
{
"source": {
"include": ["src"]
},
"opts": {
"template": "node_modules/docdash",
"destination": "./docs/"
}
}
1 change: 1 addition & 0 deletions .npmignore
@@ -0,0 +1 @@
src
1 change: 1 addition & 0 deletions README.md
@@ -0,0 +1 @@
#node-anime-list
38 changes: 38 additions & 0 deletions package.json
@@ -0,0 +1,38 @@
{
"name": "node-anime-list",
"version": "0.1.0",
"description": "Scrapper for MyAnimeList site",
"main": "dist/index.js",
"scripts": {
"test": "xo src/* && ava test/**/*.test.js --require babel-register",
"docs": "jsdoc -c .jsdocrc -r",
"build": "babel src --out-dir dist",
"prepublish": "npm run build"
},
"keywords": [
"myanimelist",
"mal",
"scrap"
],
"author": "lubien <lubien1996@gmail.com> (http://lubien.me)",
"license": "MIT",
"dependencies": {
"debug": "^2.2.0",
"got": "^6.3.0",
"xml2js-es6-promise": "^1.0.3"
},
"devDependencies": {
"ava": "^0.15.2",
"babel": "^6.5.2",
"babel-preset-es2015": "^6.9.0",
"docdash": "^0.4.0",
"dotenv": "^2.0.0",
"jsdoc": "^3.4.0",
"xo": "^0.16.0"
},
"xo": {
"rules": {
"comma-dangle": ["error", "always-multiline"]
}
}
}
31 changes: 31 additions & 0 deletions src/composers/authenticable.js
@@ -0,0 +1,31 @@
import generateAuthToken from '../utils/generate-auth-token';

const debug = require('debug')('node-anime-list:authenticable');

export default function authenticable(state) {
state.authToken = generateAuthToken(state.username, state.password);
return {
getUser() {
return state.username;
},

setUser(username, password) {
debug(`Setting '${username}' as user. Use pass = ${Boolean(password)}`);
state.username = username;
state.password = password;
state.authToken = generateAuthToken(username, password);

return this;
},

verifyCredentials() {
if (!state.username || !state.authToken) {
debug('Tried to verify a null user');
throw new Error(`Can't verify a null user`);
}

debug(`Verifying credentials for '${state.username}'`);
return this._requestApi('/account/verify_credentials.xml');
}
};
}
26 changes: 26 additions & 0 deletions src/composers/requester.js
@@ -0,0 +1,26 @@
import xml2js from 'xml2js-es6-promise';
import request from '../utils/request';
import cleanApiData from '../utils/clean-api-data';

const debug = require('debug')('node-anime-list:requester');

export default function requester(state) {
return {
_requestHtml(url = '/', query = {}) {
return request(state.authToken, url, query);
},

_requestApi(url, query = {}) {
debug('API request');

if (!state.username || !state.authToken) {
debug('Not authenticated');
throw new Error('Must have username and password set to access the API');
}

return request(state.authToken, `/api${url}`, query)
.then(res => xml2js(res.body))
.then(parsedXml => Promise.resolve(cleanApiData(parsedXml)));
}
};
}
13 changes: 13 additions & 0 deletions src/composers/searchable.js
@@ -0,0 +1,13 @@
function searchable() {
return {
searchAnimes(name) {
return this._requestApi(`/anime/search.xml`, {q: name});
},

searchMangas(name) {
return this._requestApi(`/manga/search.xml`, {q: name});
},
};
}

module.exports = searchable;
23 changes: 23 additions & 0 deletions src/index.js
@@ -0,0 +1,23 @@
import authenticable from './composers/authenticable';
import searchable from './composers/searchable';
import requester from './composers/requester';

const debug = require('debug')('node-anime-list:mal');

export default function mal(username = '', password = '') {
debug(
`New mal client user '${username}'. Use password = ${Boolean(password)}`
);

const state = {
username,
password,
authToken: '',
};

return Object.assign({},
requester(state),
authenticable(state),
searchable(state)
);
}
36 changes: 36 additions & 0 deletions src/utils/clean-api-data.js
@@ -0,0 +1,36 @@
import flattenObject from './flatten-object';

/**
* @module utils/cleanApiData
* @description Cleans XML parsed JSON from MAL.
* @requires utils/flattenObject
*
* @example
* cleanApiData({ anime: { entry: [ { key: ['value'] } ] } })
* [ { key: 'value' } ]
*
* cleanApiData({ manga: { entry: [ { key: ['value'] } ] } })
* [ { key: 'value' } ]
*
* cleanApiData({ user: { key: ['value'] } })
* { key: 'value' }
*
* @param {object} data - MyAnimeList's API parsed XML
* @return {object} - Good looking object
*/
export default function cleanApiData(data) {
if (data === null) {
return data;
}

let newData = data.anime || data.manga || data.user || data;

if (Array.isArray(newData.entry)) {
newData.entry.map(flattenObject);
newData = newData.entry;
} else if (typeof newData === 'object') {
newData = flattenObject(newData);
}

return newData;
}
24 changes: 24 additions & 0 deletions src/utils/flatten-object.js
@@ -0,0 +1,24 @@
/**
* @module utils/flattenObject
* @description Flatten XML parsed objects containing one
* element arrays as values
*
* @example
* flattenObject({a: [1], b: [2]});
* {a: 1, b: 2}
*
* flattenObject({username: ['lubien']});
* {username: 'lubien'}
*
* @param {object} obj - An object whose key's values are one element arrays
* @return {object} - Flattened object
*/
export default function flattenObject(obj) {
const newObj = {};

for (const key of Object.keys(obj)) {
newObj[key] = obj[key][0];
}

return newObj;
}
3 changes: 3 additions & 0 deletions src/utils/generate-auth-token.js
@@ -0,0 +1,3 @@
export default function generateAuthToken(username, password) {
return new Buffer(`${username}:${password}`).toString('base64');
}
14 changes: 14 additions & 0 deletions src/utils/request.js
@@ -0,0 +1,14 @@
import got from 'got';

const debug = require('debug')('node-anime-list:request');

export default function request(authToken, url = '/', query = {}) {
debug('Requesting %s with query', url, query);
debug('Using auth:', `Basic ${authToken}`);
return got(`http://myanimelist.net${url}`, {
query,
headers: {
Authorization: `Basic ${authToken}`,
},
});
}
32 changes: 32 additions & 0 deletions test/composers/authenticable.test.js
@@ -0,0 +1,32 @@
import test from 'ava';
import mal from '../../src';
import instance from '../instance';
import authenticable from '../../src/composers/authenticable';

test('Authenticable displays your username from state', t => {
const state = {username: 'lubien'};
t.is(authenticable(state).getUser(), 'lubien');
});

test('You can reset the username and password', t => {
const obj = authenticable({
username: 'lubien',
password: '',
});

obj.setUser('newuser', 'newpass');
t.is(obj.getUser(), 'newuser');
});

test('You can verify your auth', async t => {
const credentials = await instance.verifyCredentials();
t.is(credentials.username, instance.getUser());
});

test('Verifying an dummy user will throw', async t => {
try {
await mal('dummy', 'dummy').verifyCredentials();
} catch (err) {
t.pass();
}
});
25 changes: 25 additions & 0 deletions test/composers/requester.test.js
@@ -0,0 +1,25 @@
import test from 'ava';
import mal from '../../src';
import instance from '../instance';

test('Can request HTML from site', async t => {
const homepage = await instance._requestHtml('/');
t.truthy(homepage);
});

test('Can request XML from API', async t => {
const search = await instance._requestApi('/anime/search.xml', {
q: 'Full metal',
});
t.truthy(search);
});

test(`Can't request API without being authenticated`, async t => {
try {
const instanceWithoutAuth = mal();

await instanceWithoutAuth._requestApi('/anime/search.xml');
} catch (err) {
t.pass();
}
});
12 changes: 12 additions & 0 deletions test/composers/searchable.test.js
@@ -0,0 +1,12 @@
import test from 'ava';
import instance from '../instance';

test('Can search for animes', async t => {
const results = await instance.searchAnimes('Full Metal');
t.truthy(results);
});

test('Can search for mangas', async t => {
const results = await instance.searchMangas('Full Metal');
t.truthy(results);
});
15 changes: 15 additions & 0 deletions test/instance.js
@@ -0,0 +1,15 @@
import path from 'path';
import dotenv from 'dotenv';
import mal from '../src';

dotenv.config({
silent: false,
path: path.join(__dirname, '../.env'),
});

const instance = mal(
process.env.MAL_USER,
process.env.MAL_PASS
);

module.exports = instance;

0 comments on commit 9660d8d

Please sign in to comment.