diff --git a/docs/en/new-media.md b/docs/en/new-media.md index e62822bad8..5a9dc3dc65 100644 --- a/docs/en/new-media.md +++ b/docs/en/new-media.md @@ -73,6 +73,9 @@ Provides a better reading experience (full text articles) over the official one. +## Letterboxd + + ## Nautilus ### Topics diff --git a/lib/router.js b/lib/router.js index 6d39df79fc..84da151295 100644 --- a/lib/router.js +++ b/lib/router.js @@ -2170,4 +2170,7 @@ router.get('/mqube/tag/:tag', require('./routes/mqube/tag')); router.get('/mqube/latest', require('./routes/mqube/latest')); router.get('/mqube/top', require('./routes/mqube/top')); +// Letterboxd +router.get('/letterboxd/user/diary/:username', require('./routes/letterboxd/userdiary')); + module.exports = router; diff --git a/lib/routes/letterboxd/userdiary.js b/lib/routes/letterboxd/userdiary.js new file mode 100644 index 0000000000..57d4d47a34 --- /dev/null +++ b/lib/routes/letterboxd/userdiary.js @@ -0,0 +1,8 @@ +const utils = require('./utils'); + +module.exports = async (ctx) => { + const url = `https://letterboxd.com/${ctx.params.username}/films/diary/by/added/`; + const title = `Letterboxd - diary - ${ctx.params.username}`; + + ctx.state.data = await utils.getData(ctx, url, title); +}; diff --git a/lib/routes/letterboxd/utils.js b/lib/routes/letterboxd/utils.js new file mode 100644 index 0000000000..5cccb70bbc --- /dev/null +++ b/lib/routes/letterboxd/utils.js @@ -0,0 +1,102 @@ +const got = require('@/utils/got'); +const cheerio = require('cheerio'); + +async function loadReview(existingDescription, path) { + const url = `https://letterboxd.com/${path}`; + + const response = await got.get(url); + const $ = cheerio.load(response.data); + + const review = $('div.review'); + + // remove the content that we don't want to show + review.find('.hidden').remove(); + review.find('.has-spoilers').remove(); + + // generate the new description + const reviewText = review.html(); + const newDescription = existingDescription.concat(`
${reviewText}`); + + return { + description: newDescription, + }; +} + +async function ProcessFeed(list, username, caches) { + return await Promise.all( + list.map(async (item) => { + const $ = cheerio.load(item); + + const itemUrl = $('.td-film-details a').attr('href'); + + const dateString = $('.td-day > a') + .attr('href') + .replace('/' + username + '/films/diary/for/', '') + .replace(/\/$/, ''); + const pubDate = new Date(dateString).toUTCString(); + + const displayDate = new Date(dateString).toDateString(); + + const filmTitle = $('.td-film-details a').text(); + const rating = $('.td-rating .rating').text(); + const liked = $('.td-like > .has-icon').length > 0; + const rewatch = $('.td-rewatch.icon-status-off').length <= 0; + const hasReview = $('.td-review.icon-status-off').length <= 0; + + let descriptionText = `${filmTitle}
Watched: ${displayDate}
Rating: ${rating}`; + + if (liked) { + descriptionText = descriptionText.concat('
Liked'); + } + if (rewatch) { + descriptionText = descriptionText.concat('
Rewatch'); + } + + const single = { + title: filmTitle, + link: itemUrl, + author: username, + guid: itemUrl, + pubDate: pubDate, + }; + + if (hasReview) { + const description = await caches.tryGet(itemUrl, async () => await loadReview(descriptionText, itemUrl)); + + return Promise.resolve(Object.assign({}, single, description)); + } + + const description = { + description: descriptionText, + }; + + return Promise.resolve(Object.assign({}, single, description)); + }) + ); +} + +const getData = async (ctx, url, title) => { + const response = await got({ + method: 'get', + url: url, + headers: { + Referer: url, + }, + }); + + const $ = cheerio.load(response.data); + const list = $('.diary-entry-row').get(); + + const result = await ProcessFeed(list, ctx.params.username, ctx.cache); + + return { + title: title, + link: url, + description: $('meta[name="description"]').attr('content'), + item: result, + }; +}; + +module.exports = { + getData, +};