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,14 +1,12 @@
import { IGNORE_USERS, MASTODON_INSTANCE } from '$env/static/private';
import { enableVerboseLog, log } from '$lib/log';
import type { Account, Post, Tag } from '$lib/mastodon/response';
import type { Account, AccountAvatar, Post, Tag } from '$lib/mastodon/response';
import type { SongInfo } from '$lib/odesliResponse';
import { TimelineReader } from '$lib/server/timeline';
import sqlite3 from 'sqlite3';
const { DEV } = import.meta.env;
type FilterParameter = {
$limit: number | undefined | null;
$limit?: number | undefined | null;
$since?: string | undefined | null;
$before?: string | undefined | null;
[x: string]: string | number | undefined | null;
@ -44,6 +42,12 @@ type SongRow = {
thumbnailUrl?: string;
};
type AccountAvatarRow = {
account_url: string;
file: string;
sizeDescriptor: string;
};
type Migration = {
id: number;
name: string;
@ -270,6 +274,17 @@ function getMigrations(): Migration[] {
id: 4,
name: 'song info for existing posts',
statement: ``
},
{
id: 5,
name: 'resized avatars',
statement: `
CREATE TABLE accountsavatars (
file TEXT NOT NULL PRIMARY KEY,
account_url TEXT NOT NULL,
sizeDescriptor TEXT NOT NULL,
FOREIGN KEY (account_url) REFERENCES accounts(url)
);`
}
];
}
@ -278,13 +293,9 @@ async function waitReady(): Promise<void> {
// Simpler than a semaphore and is really only needed on startup
return new Promise((resolve) => {
const interval = setInterval(() => {
if (DEV) {
log.debug('Waiting for database to be ready');
}
log.verbose('Waiting for database to be ready');
if (databaseReady) {
if (DEV) {
log.debug('DB is ready');
}
log.verbose('DB is ready');
clearInterval(interval);
resolve();
}
@ -540,6 +551,40 @@ function getSongData(postIdsParams: String, postIds: string[]): Promise<Map<stri
});
}
function getAvatarData(
accountUrlsParams: String,
accountUrls: string[]
): Promise<Map<string, AccountAvatar[]>> {
return new Promise((resolve, reject) => {
db.all(
`SELECT account_url, file, sizeDescriptor
FROM accountsavatars
WHERE account_url IN (${accountUrlsParams});`,
accountUrls,
(err, rows: AccountAvatarRow[]) => {
if (err != null) {
log.error('Error loading avatars', err);
reject(err);
return;
}
const avatarMap: Map<string, AccountAvatar[]> = rows.reduce(
(result: Map<string, AccountAvatar[]>, item) => {
const info: AccountAvatar = {
accountUrl: item.account_url,
file: item.file,
sizeDescriptor: item.sizeDescriptor
};
result.set(item.account_url, [...(result.get(item.account_url) || []), info]);
return result;
},
new Map()
);
resolve(avatarMap);
}
);
});
}
export async function getPosts(
since: string | null,
before: string | null,
@ -589,6 +634,11 @@ async function getPostsInternal(
const postIds = rows.map((r: PostRow) => r.url);
const tagMap = await getTagData(postIdsParams, postIds);
const songMap = await getSongData(postIdsParams, postIds);
const accountUrls = [...new Set(rows.map((r: PostRow) => r.account_url))];
const accountUrlsParams = accountUrls.map(() => '?').join(', ');
const avatars = await getAvatarData(accountUrlsParams, accountUrls);
const posts = rows.map((row) => {
return {
id: row.id,
@ -602,10 +652,96 @@ async function getPostsInternal(
username: row.username,
display_name: row.display_name,
url: row.account_url,
avatar: row.avatar
avatar: row.avatar,
resizedAvatars: avatars.get(row.account_url) || []
} as Account,
songs: songMap.get(row.url) || []
} as Post;
});
return posts;
}
export async function removeAvatars(accountUrl: string): Promise<void> {
const params: FilterParameter = { $account: accountUrl };
const sql = `
DELETE
FROM accountsavatars
WHERE account_url = $account`;
await waitReady();
return new Promise((resolve, reject) => {
db.run(sql, params, (err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
export async function saveAvatar(avatar: AccountAvatar): Promise<void> {
// Will be null if file already existed
if (avatar.file === null) {
return;
}
const params: FilterParameter = {
$accountUrl: avatar.accountUrl,
$file: avatar.file,
$sizeDescriptor: avatar.sizeDescriptor
};
const sql = `
INSERT INTO accountsavatars
(account_url, file, sizeDescriptor) VALUES ($accountUrl, $file, $sizeDescriptor)
ON CONFLICT(file) DO UPDATE SET
account_url=excluded.account_url,
sizeDescriptor=excluded.sizeDescriptor;`;
await waitReady();
return new Promise((resolve, reject) => {
db.run(sql, params, (err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
export async function getAvatars(
accountUrl: string,
limit: number | undefined
): Promise<AccountAvatar[]> {
// TODO: Refactor to use `getAvatarData`
await waitReady();
let limitFilter = '';
const params: FilterParameter = {
$account: accountUrl,
$limit: 100
};
if (limit !== undefined) {
limitFilter = 'LIMIT $limit';
params.$limit = limit;
}
const sql = `
SELECT account_url, file, sizeDescriptor
FROM accountsavatars
WHERE account_url = $account
${limitFilter};`;
return new Promise((resolve, reject) => {
db.all(sql, params, (err, rows: AccountAvatarRow[]) => {
if (err) {
reject(err);
return;
}
resolve(
rows.map((r) => {
return {
accountUrl: r.account_url,
file: r.file,
sizeDescriptor: r.sizeDescriptor
} as AccountAvatar;
})
);
});
});
}