Compare commits

...

4 Commits

7 changed files with 108 additions and 18 deletions

View File

@ -1,4 +1,4 @@
HASHTAG_FILTER = ichlausche,music,musik,nowplaying,tunetuesday HASHTAG_FILTER = ichlausche,music,musik,nowplaying,tunetuesday,nowlistening
URL_FILTER = song.link,album.link,spotify.com,music.apple.com,bandcamp.com URL_FILTER = song.link,album.link,spotify.com,music.apple.com,bandcamp.com
YOUTUBE_API_KEY = CHANGE_ME YOUTUBE_API_KEY = CHANGE_ME
VERBOSE = false VERBOSE = false

View File

@ -26,7 +26,7 @@
gap: 10px; gap: 10px;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
background-color: #54546788; background-color: #bbbbcd73;
padding: 0.3em 1em; padding: 0.3em 1em;
margin: 0 -8px; margin: 0 -8px;
border-radius: 3px; border-radius: 3px;

View File

@ -0,0 +1,40 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let moreAvailable: boolean = false;
export let isLoading: boolean = false;
let displayText = '';
let title = '';
let disabled: boolean;
$: displayText = isLoading ? 'Loading...' : 'Load More';
$: disabled = !moreAvailable || isLoading;
$: title = moreAvailable ? 'Load More' : 'No more posts available';
const dispatch = createEventDispatcher();
function loadOlderPosts() {
dispatch('loadOlderPosts');
}
</script>
<button on:click={loadOlderPosts} {disabled} {title}>{displayText}</button>
<style>
button {
padding: 0.75em;
border-radius: 3px;
border: none;
background-color: var(--color-link);
color: white;
cursor: grab;
transition: all 0.2s ease-in-out;
}
button:hover:not(:disabled) {
background-color: var(--color-link-visited);
}
button:disabled {
cursor: not-allowed;
background-color: grey;
}
</style>

View File

@ -172,15 +172,19 @@ export function savePost(post: Post): void {
}); });
} }
export async function getPosts(since: string | null, limit: number) { export async function getPosts(since: string | null, before: string | null, limit: number) {
let promise = await new Promise<Post[]>((resolve, reject) => { let promise = await new Promise<Post[]>((resolve, reject) => {
let filter_query; let filter_query;
let params: any = { $limit: limit }; let params: any = { $limit: limit };
if (since === null) { if (since === null && before === null) {
filter_query = ''; filter_query = '';
} else { } else if (since !== null) {
filter_query = 'WHERE posts.created_at > $since'; filter_query = 'WHERE posts.created_at > $since';
params.$since = since; params.$since = since;
} else if (before !== null) {
// Setting both, before and since doesn't make sense, so this case is not explicitly handled
filter_query = 'WHERE posts.created_at < $before';
params.$before = before;
} }
const sql = `SELECT posts.id, posts.content, posts.created_at, posts.url, const sql = `SELECT posts.id, posts.content, posts.created_at, posts.url,
accounts.id AS account_id, accounts.acct, accounts.username, accounts.display_name, accounts.id AS account_id, accounts.acct, accounts.username, accounts.display_name,

View File

@ -3,7 +3,7 @@ import type { Post, Tag, TimelineEvent } from '$lib/mastodon/response';
import { savePost } from '$lib/server/db'; import { savePost } from '$lib/server/db';
import { WebSocket } from "ws"; import { WebSocket } from "ws";
const YOUTUBE_REGEX = new RegExp(/https?:\/\/(www\.)?youtu((be.com\/.*v=)|(\.be\/))(?<videoId>[a-zA-Z_0-9-]+)/gm); const YOUTUBE_REGEX = new RegExp(/https?:\/\/(www\.)?youtu((be.com\/.*?v=)|(\.be\/))(?<videoId>[a-zA-Z_0-9-]+)/gm);
export class TimelineReader { export class TimelineReader {
private static _instance: TimelineReader; private static _instance: TimelineReader;

View File

@ -4,10 +4,13 @@ import type { PageData } from './$types';
import type { Post } from '$lib/mastodon/response'; import type { Post } from '$lib/mastodon/response';
import { PUBLIC_REFRESH_INTERVAL } from '$env/static/public'; import { PUBLIC_REFRESH_INTERVAL } from '$env/static/public';
import PostComponent from '$lib/components/PostComponent.svelte'; import PostComponent from '$lib/components/PostComponent.svelte';
import LoadMoreComponent from '$lib/components/LoadMoreComponent.svelte';
export let data: PageData; export let data: PageData;
let interval: NodeJS.Timer | null = null; let interval: NodeJS.Timer | null = null;
let moreOlderPostsAvailable = true;
let loadingOlderPosts = false;
onMount(async () => { onMount(async () => {
interval = setInterval(async () => { interval = setInterval(async () => {
@ -19,8 +22,13 @@ onMount(async () => {
.then(r => r.json()) .then(r => r.json())
.then((resp: Post[]) => { .then((resp: Post[]) => {
if (resp.length > 0) { if (resp.length > 0) {
data.posts = resp.concat(data.posts); // Prepend new posts, filter dupes
console.log('updated data', resp, data.posts); // There shouldn't be any duplicates, but better be safe than sorry
const combined = resp.concat(data.posts);
const filteredPosts = combined.filter((obj, index, arr) => {
return arr.map(mapObj => mapObj.url).indexOf(obj.url) === index;
});
data.posts = filteredPosts;
} }
}) })
.catch(e => { .catch(e => {
@ -33,7 +41,39 @@ onMount(async () => {
clearInterval(interval) clearInterval(interval)
} }
} }
}) });
async function loadOlderPosts() {
loadingOlderPosts = true;
const params = new URLSearchParams();
if (data.posts.length > 0) {
params.set('before', data.posts[data.posts.length - 1].created_at);
}
await fetch(`/api/posts?${params}`)
.then(r => r.json())
.then((resp: Post[]) => {
if (resp.length > 0) {
// Append old posts, filter dupes
// There shouldn't be any duplicates, but better be safe than sorry
const combined = data.posts.concat(resp);
const filteredPosts = combined.filter((obj, index, arr) => {
return arr.map(mapObj => mapObj.url).indexOf(obj.url) === index;
});
data.posts = filteredPosts;
moreOlderPostsAvailable = true;
} else {
moreOlderPostsAvailable = false;
}
loadingOlderPosts = false;
})
.catch(e => {
loadingOlderPosts = false;
// TODO: Show error in UI
console.error('Error loading older posts', e);
});
}
</script> </script>
<svelte:head> <svelte:head>
<title>Metalhead.club music list</title> <title>Metalhead.club music list</title>
@ -45,9 +85,13 @@ onMount(async () => {
{#if data.posts.length === 0} {#if data.posts.length === 0}
Sorry, no posts recommending music aave been found yet Sorry, no posts recommending music aave been found yet
{/if} {/if}
{#each data.posts as post (post.id)} {#each data.posts as post (post.url)}
<div class="post"><PostComponent {post} /></div> <div class="post"><PostComponent {post} /></div>
{/each} {/each}
<LoadMoreComponent
on:loadOlderPosts={loadOlderPosts}
moreAvailable={moreOlderPostsAvailable}
isLoading={loadingOlderPosts}/>
</div> </div>
<div></div> <div></div>
</div> </div>
@ -55,6 +99,7 @@ onMount(async () => {
.posts { .posts {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
} }
.post { .post {
width: 100%; width: 100%;

View File

@ -5,11 +5,12 @@ import type { RequestHandler } from './$types';
export const GET = (async ({ url }) => { export const GET = (async ({ url }) => {
const since = url.searchParams.get('since'); const since = url.searchParams.get('since');
const before = url.searchParams.get('before');
let count = Number.parseInt(url.searchParams.get('count') || ''); let count = Number.parseInt(url.searchParams.get('count') || '');
if (isNaN(count)) { if (isNaN(count)) {
count = 20; count = 20;
} }
count = Math.min(count, 100); count = Math.min(count, 100);
const posts = await getPosts(since, count); const posts = await getPosts(since, before, count);
return json(posts); return json(posts);
}) satisfies RequestHandler; }) satisfies RequestHandler;