5 Commits

11 changed files with 50 additions and 38 deletions

View File

@ -10,8 +10,8 @@ BASE_URL = 'https://moshingmammut.phlaym.net'
VERBOSE = false VERBOSE = false
DEBUG_LOG = false DEBUG_LOG = false
IGNORE_USERS = @moshhead@metalhead.club IGNORE_USERS = @moshhead@metalhead.club
WEBSUB_HUB = 'http://pubsubhubbub.superfeedr.com'
PUBLIC_WEBSUB_HUB = 'http://pubsubhubbub.superfeedr.com'
PUBLIC_REFRESH_INTERVAL = 10000 PUBLIC_REFRESH_INTERVAL = 10000
PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME = 'Metalhead.club' PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME = 'Metalhead.club'
PORT = 3001 PORT = 3001

View File

@ -19,7 +19,7 @@
<meta name="theme-color" content="#17063b" media="(prefers-color-scheme: dark)" /> <meta name="theme-color" content="#17063b" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#BCB9B2" media="(prefers-color-scheme: light)" /> <meta name="theme-color" content="#BCB9B2" media="(prefers-color-scheme: light)" />
<link rel="alternate" type="application/atom+xml" href="/feed.xml" title="Atom Feed" /> <link rel="alternate" type="application/atom+xml" href="/feed.xml" title="Atom Feed" />
<link rel="hub" href="https://pubsubhubbub.superfeedr.com" /> <link rel="hub" href="%sveltekit.env.PUBLIC_WEBSUB_HUB%" />
%sveltekit.head% %sveltekit.head%
<style> <style>
body { body {

View File

@ -3,10 +3,12 @@
interface Props { interface Props {
account: Account; account: Account;
lazyLoadImages: Boolean;
} }
let { account }: Props = $props(); let { account, lazyLoadImages = true }: Props = $props();
let avatarDescription: string = $derived(`Avatar for ${account.acct}`); let avatarDescription: string = $derived(`Avatar for ${account.acct}`);
let loadingProp = $derived(lazyLoadImages ? 'lazy' : 'eager');
let sourceSetHtml: string = $derived.by(() => { let sourceSetHtml: string = $derived.by(() => {
// Sort thumbnails by file type. This is important, because the order of the srcset entries matter. // Sort thumbnails by file type. This is important, because the order of the srcset entries matter.
// We need the best format to be first // We need the best format to be first
@ -41,7 +43,7 @@
<picture> <picture>
{@html sourceSetHtml} {@html sourceSetHtml}
<img src={account.avatar} alt={avatarDescription} loading="lazy" width="50" height="50" /> <img src={account.avatar} alt={avatarDescription} loading={loadingProp} width="50" height="50" />
</picture> </picture>
<style> <style>

View File

@ -8,9 +8,10 @@
interface Props { interface Props {
post: Post; post: Post;
lazyLoadImages: Boolean;
} }
let { post }: Props = $props(); let { post, lazyLoadImages = true }: Props = $props();
let displayRelativeTime = $state(false); let displayRelativeTime = $state(false);
const absoluteDate = new Date(post.created_at).toLocaleString(); const absoluteDate = new Date(post.created_at).toLocaleString();
const timePassed = secondsSince(new Date(post.created_at)); const timePassed = secondsSince(new Date(post.created_at));
@ -20,6 +21,7 @@
} }
return absoluteDate; return absoluteDate;
}); });
let loadingProp = $derived(lazyLoadImages ? 'lazy' : 'eager');
const songs = filterDuplicates(post.songs ?? []); const songs = filterDuplicates(post.songs ?? []);
@ -115,7 +117,7 @@
</script> </script>
<div class="wrapper"> <div class="wrapper">
<div class="avatar"><AvatarComponent account={post.account} /></div> <div class="avatar"><AvatarComponent account={post.account} {lazyLoadImages} /></div>
<div class="account"><AccountComponent account={post.account} /></div> <div class="account"><AccountComponent account={post.account} /></div>
<div class="meta"> <div class="meta">
<small><a href={post.url} target="_blank" title={absoluteDate}>{dateCreated}</a></small> <small><a href={post.url} target="_blank" title={absoluteDate}>{dateCreated}</a></small>
@ -127,7 +129,12 @@
<div class="info-wrapper"> <div class="info-wrapper">
<picture> <picture>
{@html getSourceSetHtml(song)} {@html getSourceSetHtml(song)}
<img class="bgimage" src={song.thumbnailUrl} loading="lazy" alt="Blurred cover" /> <img
class="bgimage"
src={song.thumbnailUrl}
loading={loadingProp}
alt="Blurred cover"
/>
</picture> </picture>
<a href={song.pageUrl ?? song.postedUrl} target="_blank"> <a href={song.pageUrl ?? song.postedUrl} target="_blank">
<div class="info"> <div class="info">
@ -136,7 +143,7 @@
<img <img
src={song.thumbnailUrl} src={song.thumbnailUrl}
alt="Cover for {song.artistName} - {song.title}" alt="Cover for {song.artistName} - {song.title}"
loading="lazy" loading={loadingProp}
width={song.thumbnailWidth} width={song.thumbnailWidth}
height={song.thumbnailHeight} height={song.thumbnailHeight}
/> />
@ -193,6 +200,12 @@
border-radius: 3px; border-radius: 3px;
margin-bottom: 3px; margin-bottom: 3px;
} }
.cover img {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border-radius: 3px;
}
.bgimage { .bgimage {
display: none; display: none;
background-color: var(--color-bg); background-color: var(--color-bg);
@ -231,6 +244,10 @@
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
} }
.cover img {
max-width: 60px;
max-height: 60px;
}
.bgimage { .bgimage {
display: block; display: block;
width: 100%; width: 100%;

View File

@ -1037,6 +1037,12 @@ function getCachedThumbnail(thumbnailUrl: string): SongThumbnailImage[] | null {
} }
function cacheThumbnail(thumbnailUrl: string, thumbnails: SongThumbnailImage[]) { function cacheThumbnail(thumbnailUrl: string, thumbnails: SongThumbnailImage[]) {
if (!thumbnails) {
// This usually means, that the data is being saved to cached,
// while the thumbnail generation is not finished yet
logger.debug('will not cache empty thumbnail list', thumbnailUrl);
return;
}
const now = new Date().getTime(); const now = new Date().getTime();
const initialSize = thumbnailCache.size; const initialSize = thumbnailCache.size;
if (initialSize >= maxThumbnailCacheSize) { if (initialSize >= maxThumbnailCacheSize) {

View File

@ -1,4 +1,5 @@
import { BASE_URL, WEBSUB_HUB } from '$env/static/private'; import { BASE_URL } from '$env/static/private';
import { PUBLIC_WEBSUB_HUB } from '$env/static/public';
import { PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME } from '$env/static/public'; import { PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME } from '$env/static/public';
import type { Post } from '$lib//mastodon/response'; import type { Post } from '$lib//mastodon/response';
import { Logger } from '$lib/log'; import { Logger } from '$lib/log';
@ -10,7 +11,7 @@ const logger = new Logger('RSS');
export function createFeed(posts: Post[]): Feed { export function createFeed(posts: Post[]): Feed {
const baseUrl = BASE_URL.endsWith('/') ? BASE_URL : BASE_URL + '/'; const baseUrl = BASE_URL.endsWith('/') ? BASE_URL : BASE_URL + '/';
const hub = WEBSUB_HUB ? WEBSUB_HUB : undefined; const hub = PUBLIC_WEBSUB_HUB ? PUBLIC_WEBSUB_HUB : undefined;
const feed = new Feed({ const feed = new Feed({
title: `${PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME} music feed`, title: `${PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME} music feed`,
description: `Posts about music on ${PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME}`, description: `Posts about music on ${PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME}`,
@ -51,15 +52,15 @@ export function createFeed(posts: Post[]): Feed {
} }
export async function saveAtomFeed(feed: Feed) { export async function saveAtomFeed(feed: Feed) {
await fs.writeFile('feed.xml', feed.atom1(), { encoding: 'utf8' }); await fs.writeFile('feed.xml', feed.atom1(), { encoding: 'utf8' });
if (!WEBSUB_HUB || !PROD) { if (!PUBLIC_WEBSUB_HUB || !PROD) {
logger.info('Skipping Websub publish. hub configured?', WEBSUB_HUB, 'Production?', PROD); logger.info('Skipping Websub publish. hub configured?', PUBLIC_WEBSUB_HUB, 'Production?', PROD);
return; return;
} }
try { try {
const param = new FormData(); const param = new FormData();
param.append('hub.mode', 'publish'); param.append('hub.mode', 'publish');
param.append('hub.url', `${BASE_URL}/feed.xml`); param.append('hub.url', `${BASE_URL}/feed.xml`);
await fetch(WEBSUB_HUB, { await fetch(PUBLIC_WEBSUB_HUB, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: param body: param

View File

@ -335,7 +335,7 @@ export class TimelineReader {
50, 50,
3, 3,
account.url, account.url,
['webp', 'avif', 'jpeg'], ['avif', 'jpeg'],
avatar avatar
) )
); );
@ -363,7 +363,7 @@ export class TimelineReader {
200, 200,
3, 3,
song.thumbnailUrl, song.thumbnailUrl,
['webp', 'avif', 'jpeg'], ['avif', 'jpeg'],
avatar, avatar,
SongThumbnailImageKind.Big SongThumbnailImageKind.Big
) )
@ -374,7 +374,7 @@ export class TimelineReader {
60, 60,
3, 3,
song.thumbnailUrl, song.thumbnailUrl,
['webp', 'avif', 'jpeg'], ['avif', 'jpeg'],
avatar, avatar,
SongThumbnailImageKind.Small SongThumbnailImageKind.Small
) )

View File

@ -163,7 +163,7 @@
{#if posts.length === 0} {#if posts.length === 0}
Sorry, no posts recommending music have been found yet Sorry, no posts recommending music have been found yet
{/if} {/if}
{#each posts as post (post.url)} {#each posts as post, index (post.url)}
<div <div
class="post" class="post"
transition:edgeFly|global={{ transition:edgeFly|global={{
@ -173,7 +173,7 @@
easing: cubicInOut easing: cubicInOut
}} }}
> >
<PostComponent {post} /> <PostComponent {post} lazyLoadImages={index >= 4} />
</div> </div>
{/each} {/each}
<LoadMoreComponent <LoadMoreComponent

View File

@ -2,9 +2,9 @@ import type { Post } from '$lib/mastodon/response';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
export const load = (async ({ fetch, setHeaders }) => { export const load = (async ({ fetch, setHeaders }) => {
const p = await fetch('/'); const p = await fetch('/api/posts?count=5');
setHeaders({ setHeaders({
'cache-control': 'public,max-age=60' 'cache-control': 'public,max-age=300'
}); });
const j: Post[] = await p.json(); const j: Post[] = await p.json();
return { return {

View File

@ -1,17 +0,0 @@
import { Logger } from '$lib/log';
import type { RequestHandler } from './$types';
const logger = new Logger('+server.ts /');
export const GET = (async ({ fetch, setHeaders }) => {
const start = performance.now();
setHeaders({
'cache-control': 'max-age=10'
});
const afterHeaders = performance.now();
logger.debug('Headers took', afterHeaders - start, 'ms');
const f = await fetch('api/posts?count=5');
const afterFetch = performance.now();
logger.debug('Fetch took', afterFetch - afterHeaders, 'ms');
return f;
}) satisfies RequestHandler;

View File

@ -7,7 +7,10 @@ import { performance } from 'perf_hooks';
const logger = new Logger('+server.ts API'); const logger = new Logger('+server.ts API');
export const GET = (async ({ url }) => { export const GET = (async ({ url, setHeaders }) => {
setHeaders({
'cache-control': 'max-age=10'
});
const start = performance.now(); const start = performance.now();
const since = url.searchParams.get('since'); const since = url.searchParams.get('since');
const before = url.searchParams.get('before'); const before = url.searchParams.get('before');