Skip to content

Commit

Permalink
Add MyAnimeList integration
Browse files Browse the repository at this point in the history
  • Loading branch information
remixz committed Apr 17, 2017
1 parent 87233cb commit 2e5c16d
Show file tree
Hide file tree
Showing 17 changed files with 837 additions and 127 deletions.
115 changes: 29 additions & 86 deletions server/index.js
Original file line number Diff line number Diff line change
@@ -1,97 +1,40 @@
const http = require('http')
const compress = require('compression')()
const autocompleteHandler = require('./autocomplete')
const openingHandler = require('./opening')
const bifHandler = require('./bif')

const srv = http.createServer((req, res) => {
compress(req, res, () => {
if (req.url.indexOf('/autocomplete') === 0) {
autocompleteHandler(req, res)
} else if (req.url.indexOf('/opening') === 0) {
openingHandler(req, res)
} else if (req.url.indexOf('/bif') === 0) {
bifHandler(req, res)
} else if (req.url.indexOf('/update') === 0) {
updateHandler(req, res)
} else {
res.end('love arrow shoot!')
}
})
})
const express = require('express')
const compression = require('compression')
const cors = require('cors')

const setupIo = require('./io')
const autocomplete = require('./routes/autocomplete')
const bif = require('./routes/bif')
const opening = require('./routes/opening')
const mal = require('./routes/mal')

// create app
const app = express()
const srv = require('http').Server(app)
app.use(compression())
app.use(cors())

// setup socket.io
const io = require('socket.io')(srv)
io.origins('*:*')
setupIo(io)

function findRoom (rooms) {
return Object.keys(rooms).find((k) => k.startsWith('umi//'))
}

function updateHandler (req, res) {
const token = req.url.split('/update/')[1]
// setup routes
app.get('/', (req, res) => res.send({status: 'ok'}))
app.post('/update/:token', (req, res) => {
const token = req.params.token
if (token !== process.env.UPDATE_TOKEN) {
return res.end('not ok')
return res.end({status: 'not ok'})
}
io.emit('app-update')
res.end('ok')
}

io.on('connection', (socket) => {
socket.on('join-room', (room) => {
const currentRoom = findRoom(socket.rooms)
if (currentRoom) return

socket.join(room)
socket.broadcast.to(room).emit('user-joined')
const ioRoom = io.sockets.adapter.rooms[room]
if (ioRoom) {
socket.broadcast.to(room).emit('room-count', ioRoom.length)
socket.emit('room-count', ioRoom.length)
}
})

socket.on('leave-room', () => {
const room = findRoom(socket.rooms)
socket.leave(room)
socket.broadcast.to(room).emit('user-left')
const ioRoom = io.sockets.adapter.rooms[room]
if (ioRoom) {
socket.broadcast.to(room).emit('room-count', ioRoom.length)
}
})

socket.once('disconnecting', () => {
const room = findRoom(socket.rooms)
if (!room) return

socket.leave(room)
socket.broadcast.to(room).emit('user-left')
const ioRoom = io.sockets.adapter.rooms[room]
if (ioRoom) {
socket.broadcast.to(room).emit('room-count', ioRoom.length)
}
})

socket.on('update-status', (obj) => {
const room = findRoom(socket.rooms)
socket.broadcast.to(room).emit('update-status', obj)
})

socket.on('player-event', (obj) => {
const room = findRoom(socket.rooms)
socket.broadcast.to(room).emit('player-event', obj)
})

socket.on('change', (mediaId) => {
const room = findRoom(socket.rooms)
socket.broadcast.to(room).emit('change', mediaId)
})

socket.on('emoji', (name) => {
const room = findRoom(socket.rooms)
socket.broadcast.to(room).emit('emoji', name)
})
res.send({status: 'ok'})
})
app.get('/autocomplete/:country', autocomplete)
app.get('/bif', bif)
app.get('/opening', opening)
app.use('/mal', mal)

// listen
srv.listen(3001, () => {
console.log('listening on 3001')
})
64 changes: 64 additions & 0 deletions server/io.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
function findRoom (rooms) {
return Object.keys(rooms).find((k) => k.startsWith('umi//'))
}

function setupIo (io) {
io.on('connection', (socket) => {
socket.on('join-room', (room) => {
const currentRoom = findRoom(socket.rooms)
if (currentRoom) return

socket.join(room)
socket.broadcast.to(room).emit('user-joined')
const ioRoom = io.sockets.adapter.rooms[room]
if (ioRoom) {
socket.broadcast.to(room).emit('room-count', ioRoom.length)
socket.emit('room-count', ioRoom.length)
}
})

socket.on('leave-room', () => {
const room = findRoom(socket.rooms)
socket.leave(room)
socket.broadcast.to(room).emit('user-left')
const ioRoom = io.sockets.adapter.rooms[room]
if (ioRoom) {
socket.broadcast.to(room).emit('room-count', ioRoom.length)
}
})

socket.once('disconnecting', () => {
const room = findRoom(socket.rooms)
if (!room) return

socket.leave(room)
socket.broadcast.to(room).emit('user-left')
const ioRoom = io.sockets.adapter.rooms[room]
if (ioRoom) {
socket.broadcast.to(room).emit('room-count', ioRoom.length)
}
})

socket.on('update-status', (obj) => {
const room = findRoom(socket.rooms)
socket.broadcast.to(room).emit('update-status', obj)
})

socket.on('player-event', (obj) => {
const room = findRoom(socket.rooms)
socket.broadcast.to(room).emit('player-event', obj)
})

socket.on('change', (mediaId) => {
const room = findRoom(socket.rooms)
socket.broadcast.to(room).emit('change', mediaId)
})

socket.on('emoji', (name) => {
const room = findRoom(socket.rooms)
socket.broadcast.to(room).emit('emoji', name)
})
})
}

module.exports = setupIo
10 changes: 8 additions & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@
"license": "MIT",
"dependencies": {
"axios": "^0.15.3",
"body-parser": "^1.17.1",
"cheerio": "^0.22.0",
"compression": "^1.6.2",
"cors": "^2.8.3",
"express": "^4.15.2",
"iron": "^4.0.4",
"popura": "^1.2.5",
"socket.io": "^1.7.2"
},
"scripts": {
"start": "node index.js",
"dev": "cross-env UPDATE_TOKEN=example nodemon index.js"
"dev": "cross-env UPDATE_TOKEN=example IRON_TOKEN=EXAMPLE_TOKEN_PLEASE_DONT_USE_IN_PRODUCTION nodemon index.js"
},
"now": {
"alias": "umi-watch-api",
"env": {
"UPDATE_TOKEN": "@update-token"
"UPDATE_TOKEN": "@update-token",
"IRON_TOKEN": "@iron-token"
}
}
}
9 changes: 3 additions & 6 deletions server/autocomplete.js → server/routes/autocomplete.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const axios = require('axios')

function autocompleteHandler (req, res) {
const country = req.url.replace('/autocomplete/', '')
const {country} = req.params
axios.all([
axios.get(`http://because.moe/json/${country}`),
axios.get('http://www.crunchyroll.com/ajax/?req=RpcApiSearch_GetSearchCandidates')
Expand All @@ -17,13 +17,10 @@ function autocompleteHandler (req, res) {
image: c.img
}))

res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(filtered))
res.send(filtered)
})).catch((err) => {
console.error(err.message)
res.statusCode = 500
res.end('something went wrong')
res.status(500).send('something went wrong')
})
}

Expand Down
8 changes: 2 additions & 6 deletions server/bif.js → server/routes/bif.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
const axios = require('axios')
const url = require('url')
const qs = require('querystring')

function bifHandler (req, res) {
res.setHeader('Access-Control-Allow-Origin', '*')

const {query} = url.parse(req.url)
const {bif} = qs.parse(query)
const {bif} = req.query
if (!bif) return res.end()
const {hostname} = url.parse(bif)
if (!hostname.endsWith('crunchyroll.com')) return res.end()

res.setHeader('Content-Type', 'application/octet-stream')
res.type('application/octet-stream')
axios.get(bif, {
responseType: 'stream'
})
Expand Down
84 changes: 84 additions & 0 deletions server/routes/mal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const express = require('express')
const axios = require('axios')
const {json} = require('body-parser')
const popura = require('popura')
const Iron = require('iron')

const {IRON_TOKEN} = process.env

const router = express.Router()
router.use(json())

router.post('/login', function malLogin (req, res) {
const {username, password} = req.body
if (!username || !password) return res.status(400).send({status: 'not ok', error: 'Invalid login'})

const client = popura(username, password)

client.verifyAuth()
.then((obj) => {
Iron.seal(password, IRON_TOKEN, Iron.defaults, (err, sealed) => {
if (err) return res.status(500).send({status: 'not ok', error: 'Internal error'})

res.send({
status: 'ok',
username: obj.username,
password: sealed
})
})
})
.catch((err) => {
res.status(err.statusCode).send({
status: 'not ok',
error: err.message
})
})
})

router.get('/series', function malSeries (req, res) {
const {name} = req.query
if (!name) return res.status(400).send({status: 'not ok', error: 'Invalid payload'})

axios({
method: 'GET',
url: 'https://myanimelist.net/search/prefix.json',
params: {
type: 'anime',
keyword: name,
v: 1
}
})
.then(({data}) => {
const {items} = data.categories[0]

let item = items.find((i) => i.name.toLowerCase() === name.toLowerCase()) || items[0]
if (name === 'My Hero Academia Season 2') {
item = items.find((i) => i.name === 'Boku no Hero Academia 2nd Season') || item
}
if (!item) return res.status(404).send({status: 'not ok', error: `Couldn't find anime for "${name}"`})

res.send({status: 'ok', item})
})
})

router.post('/update', function malUpdate (req, res) {
const {username, password, id, episode, status} = req.body
if (!username || !password) return res.status(400).send({status: 'not ok', error: 'Invalid login'})
if (!id || !episode || !status) return res.status(400).send({status: 'not ok', error: 'Invalid payload'})

Iron.unseal(password, IRON_TOKEN, Iron.defaults, (err, unsealed) => {
if (err) return res.status(500).send({status: 'not ok', error: 'Internal error'})

const client = popura(username, unsealed)
client.updateAnime(id, {episode, status})
.then(() => {
res.send({status: 'ok'})
})
.catch((err) => {
console.error(err)
res.status(500).send({status: 'not ok', error: 'Internal error'})
})
})
})

module.exports = router
16 changes: 5 additions & 11 deletions server/opening.js → server/routes/opening.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
const url = require('url')
const qs = require('querystring')
const axios = require('axios')
const cheerio = require('cheerio')

function openingHandler (req, res) {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Content-Type', 'application/json')

const {query} = url.parse(req.url)
const {search} = qs.parse(query)
if (!search) return res.end(JSON.stringify({result: null}))
const {search} = req.query
if (!search) return res.send({result: null})

axios.post('https://themes.moe/includes/anime_search.php', `search=-1&name=${search}`).then(({data}) => {
const [html, id] = data
if (!id) return res.end(JSON.stringify({result: null}))
if (!id) return res.send({result: null})

const $ = cheerio.load(html)
const openings = $('a.vid-popup[data-type^="OP"]').toArray().map((el) => $(el).attr('href'))
const openings = $('a.vid-popup[data-type^="OP"]').toArray().map((el) => el.attribs.href)
const result = openings[Math.floor(Math.random() * openings.length)]

res.end(JSON.stringify({result}))
res.send({result})
})
}

Expand Down
Loading

0 comments on commit 2e5c16d

Please sign in to comment.