6 Commits

10 changed files with 153 additions and 44 deletions

View File

@ -1,6 +1,8 @@
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
YOUTUBE_API_KEY = CHANGE_ME
MASTODON_INSTANCE = 'metalhead.club'
VERBOSE = false
PUBLIC_REFRESH_INTERVAL = 10000
PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME = 'Metalhead.club'

View File

@ -46,32 +46,50 @@ This might not be the ideal setup to run this, but here's how I am doing it. Ins
or Debian derivative, using Apache as HTTP Proxy. Other setups are possible, but not covered here.
By default, NVM is used to install NodeJS, but you can install it any way you want.
This is based on [SvelteKit's instructions](https://kit.svelte.dev/docs/adapter-node#deploying)
This is based on [SvelteKit's instructions](https://kit.svelte.dev/docs/adapter-node#deploying) and [How To Deploy Node.js Applications Using Systemd and Nginx](https://www.digitalocean.com/community/tutorials/how-to-deploy-node-js-applications-using-systemd-and-nginx)
On your server, install the requirements:
- Apache2 HTTP Server
- NodeJS (via [NVM](https://github.com/nvm-sh/nvm))
#### On your server
Create a directory for the app. This will be called `$APP_DIR` from now on.
Install Apache2 if not already installed.
Place `package.json`, `apache2.conf.EXAMPLE`, `moshing-mammut.service.EXAMPLE` and `start.sh.EXAMPLE` in this directory.
Copy `apache2.conf.EXAMPLE` and `moshing-mammut.service.EXAMPLE` to your server.
Set up a user for the app: `useradd -mrU moshing-mammut`
Switch to your newly created user: `su moshing-mammut`
Set up NVM:
```
$ cd
$ curl https://raw.github.com/creationix/nvm/master/install.sh | sh
$ source ~/.nvm/nvm.sh
$ nvm install --lts
```
Create a directory for the app. This will be called `$APP_DIR` from now on. I use `/home/moshing-mammut/app`.
Enter `$APP_DIR`.
Place `package-lock.json` and `start.sh.EXAMPLE` in this directory.
Run `npm ci --omit dev` to install the dependencies.
Rename `start.sh.EXAMPLE` to `start.sh` and set the path to your NVM.
If you do not have NVM installed, simply remove the line and make sure your node executable can be found either by
specifying the full path or by adding it to your $PATH.
Exit out of your `moshing-mammut` user shell.
Copy `apache2.conf.EXAMPLE` to `/etc/apache2/sites-available/moshingmammut.conf` and replace `ServerName` with your
Domain. If you do not need or want SSL support, remove the whole `<IfModule mod_ssl.c>` block.
If you do, add the path to your SSLCertificateFile and SSLCertificateKeyFile.
Copy `moshing-mammut.service.EXAMPLE` to `/etc/systemd/system/moshing-mammut.service`
and replace `/PATH_TO_MOSHING_MAMMUT` with your `$APP_DIR`. Also replace `MOSHING_MAMMUT_USER` with the user you want
to run the app as.
and set your `User`, `Group`, `ExecStart` and `WorkingDirectory` accordingly.
Rename `start.sh.EXAMPLE` to `start.sh` and replace `/PATH_TO_YOUR_NVM/.nvm/nvm.sh` with the path to your NVM
installation.
If you do not have NVM installed, simply remove the line and make sure your node executable can be found either by
specifying the full path or by adding it to your $PATH.
Run `npm ci --omit dev` to install the dependencies.
#### On your development machine

View File

@ -2,14 +2,13 @@
Description=Moshing Mammut
[Service]
ExecStart=/PATH_TO_MOSHING_MAMMUT/start.sh
ExecStart=/home/moshing-mammut/app/start.sh
Restart=always
User=MOSHING_MAMMUT_USER
Group=nogroup
User=moshing-mammut
Group=moshing-mammut
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
WorkingDirectory=/PATH_TO_MOSHING_MAMMUT/
KillMode=process
WorkingDirectory=/home/moshing-mammut/app
[Install]
WantedBy=multi-user.target

View File

@ -26,7 +26,7 @@
gap: 10px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background-color: #54546788;
background-color: #bbbbcd73;
padding: 0.3em 1em;
margin: 0 -8px;
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 filter_query;
let params: any = { $limit: limit };
if (since === null) {
if (since === null && before === null) {
filter_query = '';
} else {
} else if (since !== null) {
filter_query = 'WHERE posts.created_at > $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,
accounts.id AS account_id, accounts.acct, accounts.username, accounts.display_name,

View File

@ -1,9 +1,9 @@
import { HASHTAG_FILTER, URL_FILTER, YOUTUBE_API_KEY } from '$env/static/private';
import { HASHTAG_FILTER, MASTODON_INSTANCE, URL_FILTER, YOUTUBE_API_KEY } from '$env/static/private';
import type { Post, Tag, TimelineEvent } from '$lib/mastodon/response';
import { savePost } from '$lib/server/db';
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 {
private static _instance: TimelineReader;
@ -55,7 +55,7 @@ export class TimelineReader {
}
private constructor() {
const socket = new WebSocket("wss://metalhead.club/api/v1/streaming")
const socket = new WebSocket(`wss://${MASTODON_INSTANCE}/api/v1/streaming`);
socket.onopen = (_event) => {
socket.send('{ "type": "subscribe", "stream": "public:local"}');
};

View File

@ -2,12 +2,15 @@
import { onMount } from "svelte";
import type { PageData } from './$types';
import type { Post } from '$lib/mastodon/response';
import { PUBLIC_REFRESH_INTERVAL } from '$env/static/public';
import { PUBLIC_REFRESH_INTERVAL, PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME } from '$env/static/public';
import PostComponent from '$lib/components/PostComponent.svelte';
import LoadMoreComponent from '$lib/components/LoadMoreComponent.svelte';
export let data: PageData;
let interval: NodeJS.Timer | null = null;
let moreOlderPostsAvailable = true;
let loadingOlderPosts = false;
onMount(async () => {
interval = setInterval(async () => {
@ -19,8 +22,13 @@ onMount(async () => {
.then(r => r.json())
.then((resp: Post[]) => {
if (resp.length > 0) {
data.posts = resp.concat(data.posts);
console.log('updated data', resp, data.posts);
// Prepend new posts, filter dupes
// 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 => {
@ -33,21 +41,57 @@ onMount(async () => {
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>
<svelte:head>
<title>Metalhead.club music list</title>
<title>{PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME} music list</title>
</svelte:head>
<h2>Metalhead.club music list</h2>
<h2>{PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME} music list</h2>
<div class="wrapper">
<div></div>
<div class="posts">
{#if data.posts.length === 0}
Sorry, no posts recommending music aave been found yet
{/if}
{#each data.posts as post (post.id)}
{#each data.posts as post (post.url)}
<div class="post"><PostComponent {post} /></div>
{/each}
<LoadMoreComponent
on:loadOlderPosts={loadOlderPosts}
moreAvailable={moreOlderPostsAvailable}
isLoading={loadingOlderPosts}/>
</div>
<div></div>
</div>
@ -55,6 +99,7 @@ onMount(async () => {
.posts {
display: flex;
flex-direction: column;
align-items: center;
}
.post {
width: 100%;

View File

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

View File

@ -1,3 +1,3 @@
#!/bin/bash
. /PATH_TO_YOUR_NVM/.nvm/nvm.sh
. /home/moshing-mammut/.nvm/nvm.sh
node -r dotenv/config build