Added primitive thumbnailer.

This commit is contained in:
2023-06-26 00:58:44 +02:00
parent e5b102ce07
commit 83fcdba93a
9 changed files with 2532 additions and 4 deletions

View File

@@ -1,7 +1,8 @@
// import config from 'config';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
const { argv } = yargs
const { argv } = yargs(hideBin(process.argv))
.command('npm start')
.option('log-level', {
alias: 'level',

View File

@@ -5,8 +5,13 @@ import { HttpError } from './errors';
import { fetchShelf, fetchShelves } from './shelves';
import { fetchUsers } from './users';
import { generateThumbnail } from './thumbnails';
import slugify from './utils/slugify';
import initLogger from './logger';
const logger = initLogger();
const emptyVote = {
tally: 0,
total: 0,
@@ -25,6 +30,7 @@ function curateDatabasePost(post, {
link: post.link,
shelfId: post.shelf_id,
createdAt: post.created_at,
hasThumbnail: post.thumbnail,
shelf: shelf || shelves?.[post.shelf_id],
user: users.find((user) => user.id === post.user_id),
vote: vote || votes?.[post.id] || emptyVote,
@@ -166,6 +172,8 @@ async function votePost(postId, value, user) {
.merge();
}
logger.silly(`User ${user.username} voted ${value} on post ${postId}`);
const votes = await fetchPostVotes([postId], user);
return votes[postId] || emptyVote;
@@ -198,6 +206,10 @@ async function createPost(post, shelfId, user) {
votePost(postEntry.id, 1, user),
]);
logger.verbose(`User ${user.username} created post ${postEntry.id} on s/${shelf.slug}`);
generateThumbnail(postEntry.id);
return curateDatabasePost(postEntry, { shelf, users, vote });
}

80
src/thumbnails.js Normal file
View File

@@ -0,0 +1,80 @@
import bhttp from 'bhttp';
import unprint from 'unprint';
import fs from 'fs';
import sharp from 'sharp';
import knex from './knex';
import initLogger from './logger';
const logger = initLogger();
async function createThumbnail(data, post) {
await fs.promises.mkdir('media/thumbnails', { recursive: true });
await sharp(data)
.resize(300, 300)
.jpeg({
quality: 80,
})
.toFile(`media/thumbnails/${post.id}.jpeg`);
await knex('posts')
.where('id', post.id)
.update('thumbnail', true);
logger.debug(`Saved thumbnail for ${post.id}`);
}
async function generateThumbnail(postId) {
const post = await knex('posts').where('id', postId).first();
if (!post) {
logger.warn(`Cannot generate thumbnail for non-existent post ${postId}`);
return;
}
if (!post.link) {
logger.debug(`Skipped thumbnail generation for text-only post ${postId}`);
return;
}
const res = await bhttp.get(post.link);
if (res.statusCode !== 200) {
logger.warn(`Failed thumbnail generation for ${post.link} from ${postId} (${res.statusCode})`);
return;
}
if (res.headers['content-type'].includes('image/')) {
await createThumbnail(res.body, post);
return;
}
const { query } = unprint.init(res.body.toString());
const ogHeader = query.attribute('meta[property="og:image"]', 'content');
if (ogHeader) {
const imgRes = await bhttp.get(ogHeader);
if (imgRes.statusCode === 200 && imgRes.headers['content-type'].includes('image/')) {
await createThumbnail(imgRes.body, post);
}
}
}
async function generateMissingThumbnails(force = false) {
const postsWithoutThumbnail = await knex('posts')
.select('id')
.modify((builder) => {
if (!force) {
builder.where('thumbnail', false);
}
});
await Promise.all(postsWithoutThumbnail.map(async (post) => generateThumbnail(post.id)));
}
export {
generateThumbnail,
generateMissingThumbnails,
};

View File

@@ -0,0 +1,9 @@
import { generateMissingThumbnails } from '../thumbnails';
import args from '../cli';
async function init() {
await generateMissingThumbnails(!!args.force);
process.exit();
}
init();