Convert and resize avatars to fit the displayed images
This commit is contained in:
@ -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));
|
||||
|
||||
|
Reference in New Issue
Block a user