Fix #26: Scale images to the correct size and use more efficient image formats
This commit is contained in:
@ -1,10 +1,27 @@
|
||||
import { HASHTAG_FILTER, MASTODON_INSTANCE, ODESLI_API_KEY } from '$env/static/private';
|
||||
import { log } from '$lib/log';
|
||||
import type { Account, AccountAvatar, Post, Tag, TimelineEvent } from '$lib/mastodon/response';
|
||||
import type {
|
||||
Account,
|
||||
AccountAvatar,
|
||||
Post,
|
||||
SongThumbnailImage,
|
||||
Tag,
|
||||
TimelineEvent
|
||||
} from '$lib/mastodon/response';
|
||||
import { SongThumbnailImageKind } from '$lib/mastodon/response';
|
||||
import type { OdesliResponse, Platform, SongInfo } from '$lib/odesliResponse';
|
||||
import { getAvatars, getPosts, removeAvatars, saveAvatar, savePost } from '$lib/server/db';
|
||||
import {
|
||||
getAvatars,
|
||||
getPosts,
|
||||
getSongThumbnails,
|
||||
removeAvatars,
|
||||
saveAvatar,
|
||||
savePost,
|
||||
saveSongThumbnail
|
||||
} from '$lib/server/db';
|
||||
import { createFeed, saveAtomFeed } from '$lib/server/rss';
|
||||
import { sleep } from '$lib/sleep';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs/promises';
|
||||
import sharp from 'sharp';
|
||||
import { WebSocket } from 'ws';
|
||||
@ -95,9 +112,10 @@ export class TimelineReader {
|
||||
baseName: string,
|
||||
size: number,
|
||||
suffix: string,
|
||||
folder: string,
|
||||
sharpAvatar: sharp.Sharp
|
||||
): Promise<string | null> {
|
||||
const fileName = `avatars/${baseName}_${suffix}`;
|
||||
const fileName = `${folder}/${baseName}_${suffix}`;
|
||||
const exists = await fs
|
||||
.access(fileName, fs.constants.F_OK)
|
||||
.then(() => true)
|
||||
@ -124,7 +142,13 @@ export class TimelineReader {
|
||||
for (let i = 1; i <= maxPixelDensity; i++) {
|
||||
promises.push(
|
||||
...formats.map((f) =>
|
||||
TimelineReader.resizeAvatar(avatarFilenameBase, baseSize * i, `${i}x.${f}`, sharpAvatar)
|
||||
TimelineReader.resizeAvatar(
|
||||
avatarFilenameBase,
|
||||
baseSize * i,
|
||||
`${i}x.${f}`,
|
||||
'avatars',
|
||||
sharpAvatar
|
||||
)
|
||||
.then(
|
||||
(fn) =>
|
||||
({
|
||||
@ -140,6 +164,43 @@ export class TimelineReader {
|
||||
return promises;
|
||||
}
|
||||
|
||||
private static resizeThumbnailPromiseMaker(
|
||||
filenameBase: string,
|
||||
baseSize: number,
|
||||
maxPixelDensity: number,
|
||||
songThumbnailUrl: string,
|
||||
formats: string[],
|
||||
image: ArrayBuffer,
|
||||
kind: SongThumbnailImageKind
|
||||
): Promise<void>[] {
|
||||
const sharpAvatar = sharp(image);
|
||||
const promises: Promise<void>[] = [];
|
||||
for (let i = 1; i <= maxPixelDensity; i++) {
|
||||
promises.push(
|
||||
...formats.map((f) =>
|
||||
TimelineReader.resizeAvatar(
|
||||
filenameBase,
|
||||
baseSize * i,
|
||||
`${i}x.${f}`,
|
||||
'thumbnails',
|
||||
sharpAvatar
|
||||
)
|
||||
.then(
|
||||
(fn) =>
|
||||
({
|
||||
songThumbnailUrl: songThumbnailUrl,
|
||||
file: fn,
|
||||
sizeDescriptor: `${i}x`,
|
||||
kind: kind
|
||||
} as SongThumbnailImage)
|
||||
)
|
||||
.then(saveSongThumbnail)
|
||||
)
|
||||
);
|
||||
}
|
||||
return promises;
|
||||
}
|
||||
|
||||
private static async saveAvatar(account: Account) {
|
||||
try {
|
||||
const existingAvatars = await getAvatars(account.url, 1);
|
||||
@ -176,6 +237,52 @@ export class TimelineReader {
|
||||
}
|
||||
}
|
||||
|
||||
private static async saveSongThumbnails(songs: SongInfo[]) {
|
||||
for (const song of songs) {
|
||||
if (!song.thumbnailUrl) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const existingThumbs = await getSongThumbnails(song);
|
||||
if (existingThumbs.length) {
|
||||
continue;
|
||||
}
|
||||
const fileBaseName = crypto.createHash('sha256').update(song.thumbnailUrl).digest('hex');
|
||||
const imageResponse = await fetch(song.thumbnailUrl);
|
||||
const avatar = await imageResponse.arrayBuffer();
|
||||
await Promise.all(
|
||||
TimelineReader.resizeThumbnailPromiseMaker(
|
||||
fileBaseName + '_large',
|
||||
200,
|
||||
3,
|
||||
song.thumbnailUrl,
|
||||
['webp', 'avif', 'jpeg'],
|
||||
avatar,
|
||||
SongThumbnailImageKind.Big
|
||||
)
|
||||
);
|
||||
await Promise.all(
|
||||
TimelineReader.resizeThumbnailPromiseMaker(
|
||||
fileBaseName + '_small',
|
||||
60,
|
||||
3,
|
||||
song.thumbnailUrl,
|
||||
['webp', 'avif', 'jpeg'],
|
||||
avatar,
|
||||
SongThumbnailImageKind.Small
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Could not resize and save song thumbnail for',
|
||||
song.pageUrl,
|
||||
song.thumbnailUrl,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private startWebsocket() {
|
||||
const socket = new WebSocket(`wss://${MASTODON_INSTANCE}/api/v1/streaming`);
|
||||
socket.onopen = () => {
|
||||
@ -190,9 +297,6 @@ export class TimelineReader {
|
||||
}
|
||||
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));
|
||||
|
||||
@ -206,6 +310,10 @@ export class TimelineReader {
|
||||
}
|
||||
|
||||
await savePost(post, songs);
|
||||
|
||||
await TimelineReader.saveAvatar(post.account);
|
||||
await TimelineReader.saveSongThumbnails(songs);
|
||||
|
||||
log.debug('Saved post', post.url);
|
||||
|
||||
const posts = await getPosts(null, null, 100);
|
||||
|
Reference in New Issue
Block a user