|
'use strict' |
|
|
|
// CONFIGURE YOUR SETTINGS HERE |
|
// Letterboxd username |
|
const LETTERBOXD_USERNAME = 'oan' |
|
|
|
// Which page should the script start on? |
|
// (if the importer fails somewhere) |
|
const LETTERBOXD_START_PAGE = 0 |
|
|
|
// Taste.io Authorization token |
|
// Too lazy to impl auth considering my account is facebook oauth'd |
|
const TOKEN = '' |
|
|
|
// Maps letterboxd 10 point rating scale to taste.io 4 point scale. |
|
// Adjust if desired |
|
const mapRating = function(rating) { |
|
// 1 2 3 4 5 6 7 8, 9, 10 |
|
return [null, 1, 1, 1, 2, 2, 3, 3, 4, 4, 4][rating] |
|
} |
|
|
|
// Change to true if ratings should be updated |
|
const UPDATE_RATINGS = false |
|
|
|
// ================================================================= |
|
// DOING A CODE HERE DON'T BE LOOKIN DOWN THERE LESS U LOOKIN FOR IT |
|
|
|
const through2 = require('through2') |
|
const JSONStream = require('JSONStream') |
|
const got = require('got') |
|
const cookie = require('cookie') |
|
const Xray = require('x-ray') |
|
|
|
const x = Xray() |
|
|
|
const serializeCookie = function(obj) { |
|
return Object.keys(obj).map((key) => cookie.serialize(key, obj[key])).join('; ') |
|
} |
|
|
|
// Scrape my letterboxd watchlist |
|
const pageParam = (LETTERBOXD_START_PAGE > 0) ? `page/${LETTERBOXD_START_PAGE}/` : '' |
|
const url = `http://letterboxd.com/${LETTERBOXD_USERNAME}/films/${pageParam}` |
|
x(url, '.film-list', x('.poster-container', [{ |
|
rating: '@data-owner-rating', |
|
title: '.film-poster img@alt', |
|
}])) |
|
.paginate('.paginate-nextprev .next@href') |
|
.write() |
|
// Stream the scraped titles one by one |
|
.pipe(JSONStream.parse('*')) |
|
.pipe(through2.obj(function({title, rating}, enc, cb){ |
|
if(rating == 0) { |
|
console.log(`Skipping ${title}`) |
|
return cb(null) |
|
} |
|
|
|
console.log(`Rating ${title} as ${rating} -> ${mapRating(rating)}...`) |
|
|
|
got('https://www.taste.io/api/movies/search?q=' + encodeURIComponent(title), { headers: { |
|
Authorization: 'Bearer ' + TOKEN |
|
}, json: true }) |
|
|
|
.then(res => { |
|
if(!res.body.movies.length) return |
|
const movie = res.body.movies[0] |
|
if(!UPDATE_RATINGS && movie.user && movie.user.rating) { |
|
console.log(`Skipping ${title} (already rated)`) |
|
return |
|
} |
|
|
|
const cook = cookie.parse(res.headers['set-cookie'].join('; ')) |
|
|
|
return new Promise(resolve => { |
|
const req = got.post('https://www.taste.io/api/movies/' + movie.slug + '/rating', { |
|
headers: { |
|
Authorization: 'Bearer ' + TOKEN, |
|
Cookie: serializeCookie(cook), |
|
'X-XSRF-TOKEN': cook['XSRF-TOKEN'] |
|
}, |
|
body: { |
|
rating: mapRating(rating) |
|
}, |
|
json: true |
|
}).then(res => resolve(res)).catch(err => console.error(err, req.body)) |
|
}) |
|
}) |
|
|
|
.then(_ => { |
|
cb(null, 'Success!\n') |
|
}) |
|
.catch(err => { console.error(err); cb(err) }) |
|
})) |
|
|
|
// Pipe results to stdout! |
|
.pipe(process.stdout) |
|
.on('error', err => console.error(err)) |
@pedromgapt They probably don't like sync because we (I at least) are less motivated to set other attributes on movies.
Yes, the search functionality is not optimal. With more time the script could probably be better at this. For example it could also look at next results and "fuzzy" compare the titles together with release year (+- 1 year) ๐
It would be nice to update the script. But there's a lot going on right now. Ping me over the holidays and maybe I feel more motivated ๐