15 Commits

19 changed files with 876 additions and 176 deletions

View File

@ -1,6 +1,9 @@
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'
BASE_URL = 'https://moshingmammut.phlaym.net'
VERBOSE = false
PUBLIC_REFRESH_INTERVAL = 10000
PUBLIC_REFRESH_INTERVAL = 10000
PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME = 'Metalhead.club'

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
*.db
feed.xml
node_modules
/build

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

349
package-lock.json generated
View File

@ -8,15 +8,17 @@
"name": "moshing-mammut",
"version": "0.0.1",
"dependencies": {
"@types/sqlite3": "^3.1.8",
"@types/ws": "^8.5.4",
"dotenv": "^16.0.3",
"feed": "^4.2.2",
"feeder": "git+ssh://git@phlaym.net:phlaym/feeder.git",
"sqlite3": "^5.1.6",
"ws": "^8.13.0"
},
"devDependencies": {
"@sveltejs/adapter-node": "^1.2.3",
"@sveltejs/kit": "^1.5.0",
"@types/sqlite3": "^3.1.8",
"@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"eslint": "^8.28.0",
@ -31,6 +33,102 @@
"vite": "^4.0.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.21.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz",
"integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==",
"dependencies": {
"@babel/highlight": "^7.18.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"dependencies": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/highlight/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/highlight/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.14.tgz",
@ -841,7 +939,8 @@
"node_modules/@types/node": {
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
"dev": true
},
"node_modules/@types/pug": {
"version": "2.0.6",
@ -865,6 +964,7 @@
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.8.tgz",
"integrity": "sha512-sQMt/qnyUWnqiTcJXm5ZfNPIBeJ/DVvJDwxw+0tAxPJvadzfiP1QhryO1JOR6t1yfb8NpzQb/Rud06mob5laIA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
@ -873,6 +973,7 @@
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
"integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
@ -1429,6 +1530,11 @@
"color-support": "bin.js"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@ -1536,6 +1642,14 @@
"integrity": "sha512-n94yQo4LI3w7erwf84mhRUkUJfhLoCZiLyoOZ/QFsDbcWNZePrLwbQpvZBUG2TNxwV3VjCKPxkiiQA6pe3TrTA==",
"dev": true
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -1801,6 +1915,18 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/esquery": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
@ -1922,6 +2048,38 @@
"reusify": "^1.0.4"
}
},
"node_modules/feed": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz",
"integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==",
"dependencies": {
"xml-js": "^1.6.11"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/feeder": {
"version": "1.0.0",
"resolved": "git+ssh://git@phlaym.net:phlaym/feeder.git#9543c65bd79b056202cb5649b08858e90085da41",
"license": "ISC",
"dependencies": {
"tslint": "^6.1.3",
"typescript": "^5.0.3"
}
},
"node_modules/feeder/node_modules/typescript": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.3.tgz",
"integrity": "sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=12.20"
}
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -2014,8 +2172,7 @@
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/gauge": {
"version": "3.0.2",
@ -2130,7 +2287,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
@ -2305,7 +2461,6 @@
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
"dev": true,
"dependencies": {
"has": "^1.0.3"
},
@ -2397,6 +2552,11 @@
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@ -2594,7 +2754,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -2691,7 +2850,6 @@
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"dependencies": {
"minimist": "^1.2.6"
},
@ -3009,8 +3167,7 @@
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-type": {
"version": "4.0.0",
@ -3174,7 +3331,6 @@
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
"dev": true,
"dependencies": {
"is-core-module": "^2.9.0",
"path-parse": "^1.0.7",
@ -3329,6 +3485,11 @@
"rimraf": "bin.js"
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
@ -3465,6 +3626,11 @@
"node": ">=0.10.0"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
},
"node_modules/sqlite3": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz",
@ -3580,7 +3746,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -3788,6 +3953,152 @@
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==",
"dev": true
},
"node_modules/tslint": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz",
"integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==",
"deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"builtin-modules": "^1.1.1",
"chalk": "^2.3.0",
"commander": "^2.12.1",
"diff": "^4.0.1",
"glob": "^7.1.1",
"js-yaml": "^3.13.1",
"minimatch": "^3.0.4",
"mkdirp": "^0.5.3",
"resolve": "^1.3.2",
"semver": "^5.3.0",
"tslib": "^1.13.0",
"tsutils": "^2.29.0"
},
"bin": {
"tslint": "bin/tslint"
},
"engines": {
"node": ">=4.8.0"
},
"peerDependencies": {
"typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev"
}
},
"node_modules/tslint/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/tslint/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/tslint/node_modules/builtin-modules": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
"integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tslint/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/tslint/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/tslint/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/tslint/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/tslint/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"engines": {
"node": ">=4"
}
},
"node_modules/tslint/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/tslint/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/tslint/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/tslint/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/tslint/node_modules/tsutils": {
"version": "2.29.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
"integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
"dependencies": {
"tslib": "^1.8.1"
},
"peerDependencies": {
"typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev"
}
},
"node_modules/tsutils": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
@ -3837,7 +4148,6 @@
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -4024,6 +4334,17 @@
}
}
},
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"dependencies": {
"sax": "^1.2.4"
},
"bin": {
"xml-js": "bin/cli.js"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

View File

@ -14,6 +14,8 @@
"devDependencies": {
"@sveltejs/adapter-node": "^1.2.3",
"@sveltejs/kit": "^1.5.0",
"@types/sqlite3": "^3.1.8",
"@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"eslint": "^8.28.0",
@ -29,9 +31,8 @@
},
"type": "module",
"dependencies": {
"@types/sqlite3": "^3.1.8",
"@types/ws": "^8.5.4",
"dotenv": "^16.0.3",
"feed": "^4.2.2",
"sqlite3": "^5.1.6",
"ws": "^8.13.0"
}

View File

@ -7,6 +7,7 @@
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content="#17063B" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<link rel="alternate" type="application/atom+xml" href="/feed.xml" title="Atom Feed" />
%sveltekit.head%
<style>
body {

View File

@ -1,5 +1,6 @@
import { TimelineReader } from '$lib/server/timeline';
import type { HandleServerError } from '@sveltejs/kit';
import fs from 'fs/promises';
TimelineReader.init();
@ -12,4 +13,19 @@ export const handleError = (({ error }) => {
message: 'Whoops!',
code: (error as any)?.code ?? 'UNKNOWN'
};
}) satisfies HandleServerError;
}) satisfies HandleServerError;
import type { Handle } from '@sveltejs/kit';
export const handle = (async ({ event, resolve }) => {
if (event.url.pathname === '/feed.xml') {
const f = await fs.readFile('feed.xml', { encoding: 'utf8' });
return new Response(
f,
{ headers: [['Content-Type', 'application/atom+xml']] }
);
}
const response = await resolve(event);
return response;
}) satisfies Handle;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M3 3C12.9411 3 21 11.0589 21 21H18C18 12.7157 11.2843 6 3 6V3ZM3 10C9.07513 10 14 14.9249 14 21H11C11 16.5817 7.41828 13 3 13V10ZM3 17C5.20914 17 7 18.7909 7 21H3V17Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@ -1,5 +1,6 @@
<script>
import git from '$lib/assets/git-branch-fill.svg';
import rss from '$lib/assets/rss-fill.svg';
</script>
<div class="footer">
@ -11,7 +12,14 @@
<div>
<a href="https://phlaym.net/git/phlaym/moshing-mammut">
<img alt="Git branch" src={git} class="icon" />
Source Code&nbsp;
Source Code
</a>
</div>
|
<div>
<a href="/feed.xml">
<img alt="RSS" src={rss} class="icon" />
RSS Feed
</a>
</div>
</div>
@ -26,7 +34,7 @@
gap: 10px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background-color: #54546788;
background-color: var(--color-grey-translucent);
padding: 0.3em 1em;
margin: 0 -8px;
border-radius: 3px;
@ -42,7 +50,7 @@
filter: invert();
}
.footer {
background-color: #6364FF88;
background-color: var(--color-grey-translucent);
}
}
</style>

View File

@ -0,0 +1,81 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import LoadingSpinnerComponent from '$lib/components/LoadingSpinnerComponent.svelte';
export let moreAvailable: boolean = false;
export let isLoading: boolean = false;
let displayText = '';
let title = '';
let disabled: boolean;
$: if (isLoading) {
displayText = 'Loading...';
} else if (!moreAvailable) {
displayText = 'You reached the end';
} else {
displayText = 'Load More';
};
$: disabled = !moreAvailable || isLoading;
$: title = moreAvailable ? 'Load More' : 'There be dragons!';
const dispatch = createEventDispatcher();
function loadOlderPosts() {
dispatch('loadOlderPosts');
}
</script>
<button on:click={loadOlderPosts} {disabled} {title}>
<div class="loading" class:collapsed={!isLoading}>
<LoadingSpinnerComponent size='0.5em' thickness='6px' />
</div>
<span>{displayText}</span>
</button>
<style>
button {
padding: 0.75em;
border-radius: 3px;
border: none;
background-color: var(--color-button);
color: var(--color-button-text);
cursor: grab;
transition: all 0.3s ease-in-out;
font-size: large;
font-weight: bold;
display: flex;
align-items: center;
}
button:hover:not(:disabled) {
background-color: var(--color-button-hover);
}
button:hover:not(:disabled):not(:active) {
box-shadow: 6px 6px 5px 0 var(--color-button-shadow);
translate: -2px -2px;
}
button:disabled {
cursor: not-allowed;
background-color: var(--color-button-deactivated);
}
button:not(:disabled) {
box-shadow: 4px 4px 2px 0 var(--color-button-shadow);
}
.loading {
margin-right: 3px;
display: flex;
overflow: hidden;
max-width: 100%;
transition: all 0.3s;
}
/* Cannot be removed, so that it animates its width change */
.collapsed {
max-width: 0;
margin-right: 0;
}
</style>

View File

@ -0,0 +1,38 @@
<script lang="ts">
export let size: string = '64px';
export let thickness: string = '6px';
</script>
<div class="lds-dual-ring" style="--size: {size}; --thickness: {thickness}"></div>
<style>
.lds-dual-ring {
display: inline-block;
width: 100%;
height: 100%;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: var(--size);
height: var(--size);
border-radius: 50%;
border: var(--thickness) solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(36deg);
}
75% {
transform: rotate(234deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@ -92,95 +92,112 @@ function getMigrations(): Migration[] {
}];
}
export function savePost(post: Post): void {
console.debug(`Saving post ${post.url}`);
const account = post.account;
db.run(`
INSERT INTO accounts (id, acct, username, display_name, url, avatar, avatar_static)
VALUES(?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id)
DO UPDATE SET
acct=excluded.acct,
username=excluded.username,
display_name=excluded.display_name,
url=excluded.url,
avatar=excluded.avatar,
avatar_static=excluded.avatar_static;`,
[
account.id,
account.acct,
account.username,
account.display_name,
account.url,
account.avatar,
account.avatar_static
],
(err) => {
if (err !== null) {
console.error(`Could not insert/update account ${account.id}`, err);
return;
}
db.run(`
INSERT INTO posts (id, content, created_at, url, account_id)
VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET
content=excluded.content,
created_at=excluded.created_at,
url=excluded.url,
account_id=excluded.account_id;`,
[
post.id,
post.content,
post.created_at,
post.url,
post.account.id
],
(postErr) => {
if (postErr !== null) {
console.error(`Could not insert post ${post.url}`, postErr);
return;
}
db.parallelize(() => {
for (let tag of post.tags) {
db.run(`
INSERT INTO tags (url, tag) VALUES (?, ?)
ON CONFLICT(url) DO UPDATE SET
tag=excluded.tag;`,
[
tag.url,
tag.name
],
(tagErr) => {
if (tagErr !== null) {
console.error(`Could not insert/update tag ${tag.url}`, tagErr);
return;
}
db.run('INSERT INTO poststags (post_id, tag_url) VALUES (?, ?)',
[post.id, tag.url],
(posttagserr) => {
if (posttagserr !== null) {
console.error(`Could not insert poststags ${tag.url}, ${post.url}`, posttagserr);
return;
}
}
);
}
);
export async function savePost(post: Post): Promise<undefined> {
return new Promise((resolve, reject) => {
console.debug(`Saving post ${post.url}`);
const account = post.account;
db.run(`
INSERT INTO accounts (id, acct, username, display_name, url, avatar, avatar_static)
VALUES(?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id)
DO UPDATE SET
acct=excluded.acct,
username=excluded.username,
display_name=excluded.display_name,
url=excluded.url,
avatar=excluded.avatar,
avatar_static=excluded.avatar_static;`,
[
account.id,
account.acct,
account.username,
account.display_name,
account.url,
account.avatar,
account.avatar_static
],
(err) => {
if (err !== null) {
console.error(`Could not insert/update account ${account.id}`, err);
reject(err);
return;
}
db.run(`
INSERT INTO posts (id, content, created_at, url, account_id)
VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET
content=excluded.content,
created_at=excluded.created_at,
url=excluded.url,
account_id=excluded.account_id;`,
[
post.id,
post.content,
post.created_at,
post.url,
post.account.id
],
(postErr) => {
if (postErr !== null) {
console.error(`Could not insert post ${post.url}`, postErr);
reject(postErr);
return;
}
db.parallelize(() => {
let remaining = post.tags.length;
for (let tag of post.tags) {
db.run(`
INSERT INTO tags (url, tag) VALUES (?, ?)
ON CONFLICT(url) DO UPDATE SET
tag=excluded.tag;`,
[
tag.url,
tag.name
],
(tagErr) => {
if (tagErr !== null) {
console.error(`Could not insert/update tag ${tag.url}`, tagErr);
reject(tagErr);
return;
}
db.run('INSERT INTO poststags (post_id, tag_url) VALUES (?, ?)',
[post.id, tag.url],
(posttagserr) => {
if (posttagserr !== null) {
console.error(`Could not insert poststags ${tag.url}, ${post.url}`, posttagserr);
reject(posttagserr);
return;
}
// Don't decrease on fail
remaining--;
// Only resolve after all have been inserted
if (remaining === 0) {
resolve(undefined);
}
}
);
}
);
}
});
});
});
});
});
});
}
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,

45
src/lib/server/rss.ts Normal file
View File

@ -0,0 +1,45 @@
import { BASE_URL } from '$env/static/private';
import { PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME } from '$env/static/public';
import type { Post } from '$lib//mastodon/response';
import { Feed } from 'feed';
import fs from 'fs/promises';
export function createFeed(posts: Post[]): Feed {
const baseUrl = BASE_URL.endsWith('/') ? BASE_URL : BASE_URL + '/';
const feed = new Feed({
title: `${PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME} music feed`,
description: `Posts about music on ${PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME}`,
id: baseUrl,
link: baseUrl,
language: 'en',
//image: "http://example.com/image.png",
//favicon: "http://example.com/favicon.ico",
copyright: '',
generator: 'moshing-mamut',
feedLinks: {
atom: `${BASE_URL}/feed.xml`
},
author: {
name: '@aymm',
link: 'https://metalhead.club/@aymm'
},
});
posts.forEach(p => {
feed.addItem({
title: p.content,
id: p.url,
link: p.url,
content: p.content,
author: [{
name: p.account.acct,
link: p.account.url
}],
date: new Date(p.created_at)
})
});
feed.addCategory('Music');
return feed;
}
export async function saveAtomFeed(feed: Feed) {
await fs.writeFile('feed.xml', feed.atom1(), { encoding: 'utf8' });
}

View File

@ -1,9 +1,10 @@
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 { getPosts, savePost } from '$lib/server/db';
import { createFeed, saveAtomFeed } from '$lib/server/rss';
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 +56,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"}');
};
@ -79,7 +80,9 @@ export class TimelineReader {
!await TimelineReader.checkYoutubeMatches(post.content)) {
return;
}
savePost(post);
await savePost(post);
const posts = await getPosts(null, null, 100);
await saveAtomFeed(createFeed(posts));
} catch (e) {
console.error("error message", event, event.data, e)

View File

@ -2,52 +2,170 @@
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';
import { fly, type FlyParams } from 'svelte/transition';
import { cubicInOut } from 'svelte/easing';
export let data: PageData;
interface FetchOptions {
since?: string,
before?: string,
count?: number
}
interface EdgeFlyParams extends FlyParams {
created_at: string
}
const refreshInterval = parseInt(PUBLIC_REFRESH_INTERVAL) * 10;
let interval: NodeJS.Timer | null = null;
let moreOlderPostsAvailable = true;
let loadingOlderPosts = false;
// Needed, so that edgeFly() can do its thing:
// To determine whether a newly loaded post is older than the existing ones, is required to know what the oldest
// post was, before the fetch happened.
let oldestBeforeLastFetch: number | null = null;
/**
* Animate either from the top, or the bottom of the window, depending if the post is
* newer than the existing ones or older.
*/
function edgeFly(node: Element, opts: EdgeFlyParams) {
const createdAt = new Date(opts.created_at).getTime();
const diffNewest = Math.abs(new Date(data.posts[0].created_at).getTime() - createdAt);
const oldest = oldestBeforeLastFetch !== null
? oldestBeforeLastFetch
: new Date(data.posts[data.posts.length - 1].created_at).getTime();
const diffOldest = Math.abs(oldest - createdAt);
const fromTop = diffNewest <= diffOldest;
const rect = node.getBoundingClientRect();
const paramY = +`${opts.y}`;
let offset = isNaN(paramY) ? 0 : paramY + rect.height;
opts.y = fromTop ? -offset : window.innerHeight + offset;
return fly(node, opts);
}
async function fetchPosts(options: FetchOptions): Promise<Post[]> {
const params = new URLSearchParams();
if (options?.since !== undefined) {
params.set('since', options.since);
}
if (options?.before !== undefined) {
params.set('before', options.before);
}
if (options?.count !== undefined) {
params.set('count', options.count.toFixed(0));
}
const response = await fetch(`/api/posts?${params}`);
return await response.json();
}
function filterDuplicates(posts: Post[]): Post[] {
return posts.filter((obj, index, arr) => {
return arr.map(mapObj => mapObj.url).indexOf(obj.url) === index;
});
}
function refresh() {
let filter: FetchOptions = {};
if (data.posts.length > 0) {
filter = { since: data.posts[0].created_at };
}
fetchPosts(filter).then(resp => {
if (resp.length > 0) {
// Prepend new posts, filter dupes
// There shouldn't be any duplicates, but better be safe than sorry
data.posts = filterDuplicates(resp.concat(data.posts));
}
})
.catch(e => {
// TODO: Show error in UI
console.error('Error loading newest posts', e);
});
}
onMount(async () => {
interval = setInterval(async () => {
const params = new URLSearchParams();
if (data.posts.length > 0) {
params.set('since', data.posts[0].created_at);
if (data.posts.length > 0) {
oldestBeforeLastFetch = new Date(data.posts[data.posts.length - 1].created_at).getTime();
}
interval = setInterval(refresh, refreshInterval);
// - If the page is hidden, slow down refresh rate
// - If the page is shown, bump up refresh rate
document.addEventListener('visibilitychange', () => {
const delay = document.hidden ? refreshInterval * 10 : refreshInterval;
if (interval) {
clearInterval(interval);
}
await fetch(`/api/posts?${params}`)
.then(r => r.json())
.then((resp: Post[]) => {
if (resp.length > 0) {
data.posts = resp.concat(data.posts);
console.log('updated data', resp, data.posts);
}
})
.catch(e => {
// TODO: Show error in UI
console.error('Error loading newest posts', e);
});
}, parseInt(PUBLIC_REFRESH_INTERVAL));
interval = setInterval(refresh, delay);
});
return () => {
if (interval !== null) {
clearInterval(interval)
}
}
})
});
function loadOlderPosts() {
loadingOlderPosts = true;
const filter: FetchOptions = { count: 20 };
if (data.posts.length > 0) {
filter.before = data.posts[data.posts.length - 1].created_at;
oldestBeforeLastFetch = new Date(filter.before).getTime();
}
fetchPosts(filter).then(resp => {
if (resp.length > 0) {
// Append old posts, filter dupes
// There shouldn't be any duplicates, but better be safe than sorry
data.posts = filterDuplicates(data.posts.concat(resp));
// If we got less than we expected, there are no older posts available
moreOlderPostsAvailable = resp.length < (filter.count ?? 20);
} 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)}
<div class="post"><PostComponent {post} /></div>
{#each data.posts as post (post.url)}
<div
class="post"
transition:edgeFly="{{ y: 10, created_at: post.created_at, duration: 300, easing: cubicInOut }}"
>
<PostComponent {post} />
</div>
{/each}
<LoadMoreComponent
on:loadOlderPosts={loadOlderPosts}
moreAvailable={moreOlderPostsAvailable}
isLoading={loadingOlderPosts}/>
</div>
<div></div>
</div>
@ -55,6 +173,7 @@ onMount(async () => {
.posts {
display: flex;
flex-direction: column;
align-items: center;
}
.post {
width: 100%;
@ -70,5 +189,6 @@ onMount(async () => {
h2 {
text-align: center;
z-index: 100;
}
</style>

View File

@ -4,12 +4,13 @@ import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET = (async ({ url }) => {
const since = url.searchParams.get('since');
let count = Number.parseInt(url.searchParams.get('count') || '');
if (isNaN(count)) {
count = 20;
}
count = Math.min(count, 100);
const posts = await getPosts(since, count);
return json(posts);
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, 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

View File

@ -1,30 +1,56 @@
body {
--color-text: #2F0C7A;
--color-bg: white;
--color-border: #17063B;
--color-link: #563ACC;
--color-link-visited: #858AFA;
--color-blue: hsl(259, 82%, 26%);
--color-blue-dark: hsl(259, 82%, 10%);
--color-lavender: hsl(273, 43%, 65%);
--color-mauve: hsl(286, 73%, 81%);
color: var(--color-text);
background-color: var(--color-bg);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans,
Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
--color-grey: hsl(44, 7%, 41%);
--color-grey-translucent: hsla(44, 7%, 41%, 0.2);
--color-grey-light: hsl(42, 7%, 72%);
--color-red: hsl(7, 100%, 56%);
--color-red-light: hsl(7, 100%, 61%);
--color-red-desat: hsl(7, 20%, 56%);
--color-red-desat-dark: hsl(7, 20%, 30%);
--color-red-desat-desat: hsl(7, 8%, 56%);
--color-text: var(--color-blue);
--color-border: var(--color-grey);
--color-link: var(--color-mauve);
--color-link-visited: var(--color-lavender);
--color-bg: var(--color-grey-light);
--color-button: var(--color-red-light);
--color-button-shadow: var(--color-red-desat-dark);
--color-button-hover: var(--color-red);
--color-button-deactivated: var(--color-red-desat-desat);
--color-button-text: var(--color-blue-dark);
color: var(--color-text);
background-color: var(--color-bg);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans,
Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
a {
color: var(--color-link);
color: var(--color-link);
}
a:visited {
color: var(--color-link-visited);
color: var(--color-link-visited);
}
@media (prefers-color-scheme: dark) {
body {
--color-text: white;
--color-bg: #17063B;
--color-border: white;
--color-link: #8A9BF0;
--color-link-visited: #C384FB;
}
body {
--color-text: var(--color-grey-light);
--color-border: var(--color-grey-light);
--color-link: var(--color-mauve);
--color-link-visited: var(--color-lavender);
--color-bg: var(--color-blue);
--color-button: var(--color-red-light);
--color-button-shadow: var(--color-red-desat);
--color-button-hover: var(--color-red);
--color-button-deactivated: var(--color-red-desat-desat);
--color-button-text: var(--color-blue-dark);
}
}