Skip to content

Instantly share code, notes, and snippets.

@qgustavor qgustavor/index.js

Last active Feb 17, 2016
Embed
What would you like to do?
Hummingbird to MAL
// This script tries to follow Standard Code Style
var fs = require('fs-extra')
var request = require('request')
.defaults({
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; HummingSync/1.4.0-gist; +https://hummingbird.me/stories/5534175)'
}
})
var state = {
lastUpdate: '',
credentials: {},
errors: [],
ids: []
}
var stateConversionMap = {
'currentlywatching': 1,
'plantowatch': 6,
'completed': 2,
'onhold': 3,
'dropped': 4
}
var cachedLibrary
function saveState() {
console.log('Saving state')
fs.outputJson('state.json', state, function (err) {
if (err) {
console.log('Error when saving state, retrying')
setTimeout(saveState, 1000 + Math.random() * 2000)
}
})
}
function handleError(error) {
console.error('Got error:', error)
state.errors.push(new Date().toISOString() + ' ' + error.toString())
if (state.errors.length > 10) {
state.errors.splice(0, state.errors - 10)
}
}
function processMedia(mediaList) {
if (mediaList.length === 0) {
console.log('All updates processed')
saveState()
return
}
var element = mediaList.pop()
console.log('Processing', element['media']['title'])
var statusChange = false
var episode
var status
element['substories'].reverse().forEach(function(story) {
if (story['substory_type'] === 'watched_episode') {
episode = story['episode_number']
}
if (story['substory_type'] === 'watchlist_status_update') {
status = stateConversionMap[story['new_status'].toLowerCase().replace(/[^a-z]/g, '')]
statusChange = true
}
})
element.episode = episode
element.status = status
// status = 2 => completed
if (statusChange && cachedLibrary) {
updateExtraValues()
updateMal(mediaList, element)
} else if (statusChange) {
request('https://hummingbird.me/api/v1/users/qgustavor/library', function (err, response, body) {
try {
body = JSON.parse(body)
} catch (e) {
handleError(e)
updateMal(mediaList, element)
return
}
cachedLibrary = body
updateExtraValues()
updateMal(mediaList, element)
})
} else {
updateMal(mediaList, element)
}
function updateExtraValues() {
var el = cachedLibrary.filter(function (e) {
return e['anime']['id'] === element['media']['id']
})[0]
if (el) {
if (+el['episodes_watched'] === 1) {
element.start_date = el['last_watched'].replace(/^(\d\d\d\d)-(\d\d)-(\d\d).*/,'$2$3$1')
}
if (el['status'] === 'completed') {
element.finished_date = el['last_watched'].replace(/^(\d\d\d\d)-(\d\d)-(\d\d).*/,'$2$3$1')
element.episode = el['anime']['episode_count']
} else {
element.episode = el['episodes_watched']
}
element.enable_rewatching = el['rewatching'] ? 1 : 0
element.times_rewatched = el['rewatched_times']
element.score = parseFloat(el['rating']['value']) * 2 // Convert from 0-5 score to 0-10 score
}
}
}
function updateMal(mediaList, element) {
var elementId = element['media']['mal_id']
var isNew = state.ids.indexOf(elementId) === -1
if (elementId == null) {
handleError('elementId is null, id: ' + element['id'])
processMedia(mediaList)
return
}
if (isNew) {
state.ids.push(elementId)
}
request({
method: 'POST',
url: 'http://myanimelist.net/api/animelist/' +
(isNew ? 'add' : 'update') +
'/' + elementId + '.xml',
auth: {
user: state.credentials.username,
pass: Buffer(state.credentials.password, 'base64').toString(),
sendImmediately: true
},
form: {
data: '<?xml version="1.0" encoding="UTF-8"?><entry>' +
(element.episode == null ? '' : ('<episode>' + element.episode + '</episode>')) +
(element.status == null ? '' : ('<status>' + element.status + '</status>')) +
(element.score == null ? '' : ('<score>' + element.score + '</score>')) +
(element.enable_rewatching == null ? '' : ('<enable_rewatching>' + element.enable_rewatching + '</enable_rewatching>')) +
(element.times_rewatched == null ? '' : ('<times_rewatched>' + element.times_rewatched + '</times_rewatched>')) +
(element.start_date == null ? '' : ('<date_start>' + element.start_date + '</date_start>')) +
(element.finished_date == null ? '' : ('<date_finish>' + element.finished_date + '</date_finish>')) +
'</entry>'
}
}, function (err, response, body) {
if (err || response.statusCode >= 300) {
handleError(body || err || response)
}
processMedia(mediaList)
})
}
function isProcessableStory(story) {
return story['substory_type'] === 'watched_episode' || story['substory_type'] === 'watchlist_status_update'
}
function getFeed() {
console.log('Getting update feed')
request('https://hummingbird.me/api/v1/users/qgustavor/feed', function (err, response, body) {
if (err || response.statusCode >= 300) {
handleError(err ? err : body)
saveState()
return
}
try {
body = JSON.parse(body)
} catch(e) {
handleError('HummingBird feed parsing failed')
saveState()
return
}
var updatedMedia = body.filter(function (element) {
return element['story_type'] === 'media_story' &&
element['updated_at'] > state.lastUpdate &&
element['substories'].filter(isProcessableStory).length > 0
})
if (updatedMedia.length === 0) {
console.log('No entries')
return
}
console.log('Got', updatedMedia.length, 'new entries')
state.lastUpdate = updatedMedia.reduce(function (currentDate, element) {
return currentDate > element['updated_at'] ? currentDate : element['updated_at']
}, state.lastUpdate)
processMedia(updatedMedia)
})
}
function getState(errorCount) {
console.log('Getting state')
fs.readJSON('state.json', function (err, result) {
if (err && errorCount < 10) {
console.error('Could not get state')
process.exit(1)
} else if (err) {
setTimeout(getState, errorCount * 1000, errorCount)
} else {
for (var key in result) {
state[key] = result[key]
}
getFeed()
}
})
}
getState(0)
{
"lastUpdate": "2016-02-16T16:37:47.839Z",
"credentials": {
"username": "your username",
"password": "your password (base64 obfuscated)"
},
"errors": [],
"ids": []
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.