Convert and resize avatars to fit the displayed images

This commit is contained in:
2023-05-02 17:31:16 +02:00
parent fbaedaf45b
commit 736b8498af
10 changed files with 797 additions and 84 deletions

View File

@ -1,10 +1,12 @@
import { HASHTAG_FILTER, MASTODON_INSTANCE, ODESLI_API_KEY } from '$env/static/private';
import { log } from '$lib/log';
import type { Post, Tag, TimelineEvent } from '$lib/mastodon/response';
import type { Account, AccountAvatar, Post, Tag, TimelineEvent } from '$lib/mastodon/response';
import type { OdesliResponse, Platform, SongInfo } from '$lib/odesliResponse';
import { getPosts, savePost } from '$lib/server/db';
import { getAvatars, getPosts, removeAvatars, saveAvatar, savePost } from '$lib/server/db';
import { createFeed, saveAtomFeed } from '$lib/server/rss';
import { sleep } from '$lib/sleep';
import fs from 'fs/promises';
import sharp from 'sharp';
import { WebSocket } from 'ws';
const URL_REGEX = new RegExp(/href="(?<postUrl>[^>]+?)" target="_blank"/gm);
@ -89,6 +91,144 @@ export class TimelineReader {
}
}
private static async resizeAvatar(
baseName: string,
size: number,
suffix: string,
sharpAvatar: sharp.Sharp
): Promise<string | null> {
const fileName = `avatars/${baseName}_${suffix}`;
const exists = await fs
.access(fileName, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
if (exists) {
log.debug('File already exists', fileName);
return null;
}
log.debug('Saving avatar', fileName);
await sharpAvatar.resize(size).toFile(fileName);
return fileName;
}
private static async saveAvatar(account: Account) {
try {
const existingAvatars = await getAvatars(account.url, 1);
const existingAvatarBase = existingAvatars.shift()?.file.split('/').pop()?.split('_').shift();
const avatarFilenameBase =
new URL(account.avatar).pathname.split('/').pop()?.split('.').shift() ?? account.acct;
// User's avatar changed. Remove the old one!
if (existingAvatarBase && existingAvatarBase !== avatarFilenameBase) {
await removeAvatars(account.url);
const avatarsToDelete = (await fs.readdir('avatars'))
.filter((x) => x.startsWith(existingAvatarBase + '_'))
.map((x) => {
log.debug('Removing existing avatar file', x);
return x;
})
.map((x) => fs.unlink('avatars/' + x));
await Promise.allSettled(avatarsToDelete);
}
const avatarResponse = await fetch(account.avatar);
const avatar = await avatarResponse.arrayBuffer();
const sharpAvatar = sharp(avatar);
await Promise.all([
TimelineReader.resizeAvatar(avatarFilenameBase, 50, '1x.webp', sharpAvatar)
.then(
(fn) =>
({
accountUrl: account.url,
file: fn,
sizeDescriptor: '1x'
} as AccountAvatar)
)
.then(saveAvatar),
TimelineReader.resizeAvatar(avatarFilenameBase, 100, '2x.webp', sharpAvatar)
.then(
(fn) =>
({
accountUrl: account.url,
file: fn,
sizeDescriptor: '2x'
} as AccountAvatar)
)
.then(saveAvatar),
TimelineReader.resizeAvatar(avatarFilenameBase, 150, '3x.webp', sharpAvatar)
.then(
(fn) =>
({
accountUrl: account.url,
file: fn,
sizeDescriptor: '3x'
} as AccountAvatar)
)
.then(saveAvatar),
TimelineReader.resizeAvatar(avatarFilenameBase, 50, '1x.avif', sharpAvatar)
.then(
(fn) =>
({
accountUrl: account.url,
file: fn,
sizeDescriptor: '1x'
} as AccountAvatar)
)
.then(saveAvatar),
TimelineReader.resizeAvatar(avatarFilenameBase, 100, '2x.avif', sharpAvatar)
.then(
(fn) =>
({
accountUrl: account.url,
file: fn,
sizeDescriptor: '2x'
} as AccountAvatar)
)
.then(saveAvatar),
TimelineReader.resizeAvatar(avatarFilenameBase, 150, '3x.avif', sharpAvatar)
.then(
(fn) =>
({
accountUrl: account.url,
file: fn,
sizeDescriptor: '3x'
} as AccountAvatar)
)
.then(saveAvatar),
TimelineReader.resizeAvatar(avatarFilenameBase, 50, '1x.jpeg', sharpAvatar)
.then((fn) => {
return {
accountUrl: account.url,
file: fn,
sizeDescriptor: '1x'
} as AccountAvatar;
})
.then(saveAvatar),
TimelineReader.resizeAvatar(avatarFilenameBase, 100, '2x.jpeg', sharpAvatar)
.then(
(fn) =>
({
accountUrl: account.url,
file: fn,
sizeDescriptor: '2x'
} as AccountAvatar)
)
.then(saveAvatar),
TimelineReader.resizeAvatar(avatarFilenameBase, 150, '3x.jpeg', sharpAvatar)
.then(
(fn) =>
({
accountUrl: account.url,
file: fn,
sizeDescriptor: '3x'
} as AccountAvatar)
)
.then(saveAvatar)
]);
} catch (e) {
console.error('Could not resize and save avatar for', account.acct, account.avatar, e);
}
}
private startWebsocket() {
const socket = new WebSocket(`wss://${MASTODON_INSTANCE}/api/v1/streaming`);
socket.onopen = () => {
@ -102,6 +242,10 @@ export class TimelineReader {
return;
}
const post: Post = JSON.parse(data.payload);
// TODO: Move down after post has been confirmed after testing
await TimelineReader.saveAvatar(post.account);
const hashttags: string[] = HASHTAG_FILTER.split(',');
const found_tags: Tag[] = post.tags.filter((t: Tag) => hashttags.includes(t.name));