+1
deno.json
+1
deno.json
···
1
+
{}
+10
-24
deno.lock
+10
-24
deno.lock
···
22
22
"npm:mdsvex@~0.12.6": "0.12.6_svelte@5.39.11__acorn@8.15.0",
23
23
"npm:nanoid@^5.1.5": "5.1.6",
24
24
"npm:node-fetch@^3.3.2": "3.3.2",
25
-
"npm:node-schedule@^2.1.1": "2.1.1",
26
25
"npm:postcss@^8.5.6": "8.5.6",
27
26
"npm:prettier-plugin-svelte@^3.4.0": "3.4.0_prettier@3.6.2_svelte@5.39.11__acorn@8.15.0",
28
27
"npm:prettier@^3.6.2": "3.6.2",
···
33
32
"npm:svelte@^5.38.2": "5.39.11_acorn@8.15.0",
34
33
"npm:sveltekit-rate-limiter@0.7": "0.7.0_@sveltejs+kit@2.46.4__@sveltejs+vite-plugin-svelte@6.2.1___svelte@5.39.11____acorn@8.15.0___vite@7.1.9____@types+node@22.18.9____picomatch@4.0.3___@types+node@22.18.9__svelte@5.39.11___acorn@8.15.0__vite@7.1.9___@types+node@22.18.9___picomatch@4.0.3__acorn@8.15.0__@types+node@22.18.9_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.39.11___acorn@8.15.0__vite@7.1.9___@types+node@22.18.9___picomatch@4.0.3__@types+node@22.18.9_svelte@5.39.11__acorn@8.15.0_vite@7.1.9__@types+node@22.18.9__picomatch@4.0.3_@types+node@22.18.9",
35
34
"npm:tailwindcss@^3.4.17": "3.4.18_postcss@8.5.6_jiti@1.21.7",
35
+
"npm:toad-scheduler@^3.1.0": "3.1.0",
36
36
"npm:tslib@^2.8.1": "2.8.1",
37
37
"npm:typescript-eslint@^8.40.0": "8.46.0_eslint@9.37.0_typescript@5.9.3_@typescript-eslint+parser@8.46.0__eslint@9.37.0__typescript@5.9.3",
38
38
"npm:typescript-svelte-plugin@~0.3.50": "0.3.50_svelte@5.39.11__acorn@8.15.0_typescript@5.9.3",
···
1073
1073
"cookie@0.6.0": {
1074
1074
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
1075
1075
},
1076
-
"cron-parser@4.9.0": {
1077
-
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
1078
-
"dependencies": [
1079
-
"luxon"
1080
-
]
1076
+
"croner@8.1.2": {
1077
+
"integrity": "sha512-ypfPFcAXHuAZRCzo3vJL6ltENzniTjwe/qsLleH1V2/7SRDjgvRQyrLmumFTLmjFax4IuSxfGXEn79fozXcJog=="
1081
1078
},
1082
1079
"cross-spawn@7.0.6": {
1083
1080
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
···
1646
1643
"lodash.merge@4.6.2": {
1647
1644
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
1648
1645
},
1649
-
"long-timeout@0.1.1": {
1650
-
"integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w=="
1651
-
},
1652
1646
"long@5.3.2": {
1653
1647
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
1654
1648
},
1655
1649
"lru-cache@10.4.3": {
1656
1650
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
1657
-
},
1658
-
"luxon@3.7.2": {
1659
-
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="
1660
1651
},
1661
1652
"magic-string@0.30.19": {
1662
1653
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
···
1763
1754
"node-releases@2.0.23": {
1764
1755
"integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="
1765
1756
},
1766
-
"node-schedule@2.1.1": {
1767
-
"integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==",
1768
-
"dependencies": [
1769
-
"cron-parser",
1770
-
"long-timeout",
1771
-
"sorted-array-functions"
1772
-
]
1773
-
},
1774
1757
"normalize-path@3.0.0": {
1775
1758
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
1776
1759
},
···
2122
2105
},
2123
2106
"snappyjs@0.6.1": {
2124
2107
"integrity": "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg=="
2125
-
},
2126
-
"sorted-array-functions@1.3.0": {
2127
-
"integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA=="
2128
2108
},
2129
2109
"source-map-js@1.2.1": {
2130
2110
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
···
2310
2290
"is-number"
2311
2291
]
2312
2292
},
2293
+
"toad-scheduler@3.1.0": {
2294
+
"integrity": "sha512-ZTwsGMWyKTOokgTmIvjPIvkT3ZiPFgkAi8L0OLONOcSc/BUDPRzNMOfVWZzugIAxyntvY0Nzy1etNk+31Q4FXQ==",
2295
+
"dependencies": [
2296
+
"croner"
2297
+
]
2298
+
},
2313
2299
"totalist@3.0.1": {
2314
2300
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="
2315
2301
},
···
2502
2488
"npm:mdsvex@~0.12.6",
2503
2489
"npm:nanoid@^5.1.5",
2504
2490
"npm:node-fetch@^3.3.2",
2505
-
"npm:node-schedule@^2.1.1",
2506
2491
"npm:postcss@^8.5.6",
2507
2492
"npm:prettier-plugin-svelte@^3.4.0",
2508
2493
"npm:prettier@^3.6.2",
···
2513
2498
"npm:svelte@^5.38.2",
2514
2499
"npm:sveltekit-rate-limiter@0.7",
2515
2500
"npm:tailwindcss@^3.4.17",
2501
+
"npm:toad-scheduler@^3.1.0",
2516
2502
"npm:tslib@^2.8.1",
2517
2503
"npm:typescript-eslint@^8.40.0",
2518
2504
"npm:typescript-svelte-plugin@~0.3.50",
+2
-2
package.json
+2
-2
package.json
···
48
48
"@types/node-schedule": "^2.1.8",
49
49
"nanoid": "^5.1.5",
50
50
"node-fetch": "^3.3.2",
51
-
"node-schedule": "^2.1.1",
52
51
"prometheus-remote-write": "^0.5.1",
53
52
"robots-parser": "^3.0.1",
54
-
"steamgriddb": "^2.2.0"
53
+
"steamgriddb": "^2.2.0",
54
+
"toad-scheduler": "^3.1.0"
55
55
},
56
56
"trustedDependencies": [
57
57
"@sveltejs/kit",
+16
-19
src/hooks.server.ts
+16
-19
src/hooks.server.ts
···
1
1
import { updateLastPosts } from '$lib/bluesky';
2
-
import { lastFmReadLast, lastFmUpdateNowPlaying } from '$lib/lastfm';
2
+
import { getLastTrack, updateNowPlayingTrack } from '$lib/lastfm';
3
3
import { steamReadLastGame, steamUpdateNowPlaying } from '$lib/steam';
4
4
import { updateCommits } from '$lib/activity';
5
-
import { cancelJob, scheduleJob, scheduledJobs } from 'node-schedule';
5
+
import { ToadScheduler, SimpleIntervalJob, Task, AsyncTask } from 'toad-scheduler';
6
6
import {
7
7
incrementFakeVisitCount,
8
8
incrementLegitVisitCount,
···
20
20
import { error } from '@sveltejs/kit';
21
21
import { _fetchEntries } from './routes/(site)/guestbook/+page.server';
22
22
23
-
const UPDATE_LAST_JOB_NAME = 'update steam game, lastfm track, bsky posts, git activity';
24
-
25
-
if (UPDATE_LAST_JOB_NAME in scheduledJobs) {
26
-
console.log(`${UPDATE_LAST_JOB_NAME} is already running, cancelling so we can start a new one`);
27
-
cancelJob(UPDATE_LAST_JOB_NAME);
28
-
}
29
-
30
-
await steamReadLastGame();
31
-
await lastFmReadLast();
32
-
33
-
console.log(`starting ${UPDATE_LAST_JOB_NAME} job...`);
34
-
scheduleJob(UPDATE_LAST_JOB_NAME, '*/1 * * * *', async () => {
35
-
console.log(`running ${UPDATE_LAST_JOB_NAME} job...`);
23
+
const update = async () => {
36
24
try {
37
25
await Promise.all([
38
26
steamUpdateNowPlaying(),
39
-
lastFmUpdateNowPlaying(),
27
+
updateNowPlayingTrack(),
40
28
updateLastPosts(),
41
29
_fetchEntries(),
42
30
updateCommits(),
43
-
sendAllMetrics() // send all metrics every minute
31
+
sendAllMetrics()
44
32
]);
45
33
} catch (err) {
46
-
console.log(`error while running ${UPDATE_LAST_JOB_NAME} job: ${err}`);
34
+
console.log(`error while updating: ${err}`);
47
35
}
48
-
}).invoke(); // invoke once immediately
36
+
};
37
+
38
+
await update();
39
+
40
+
const scheduler = new ToadScheduler();
41
+
const task = new AsyncTask('update task', update, (err) =>
42
+
console.log(`error while updating: ${err}`)
43
+
);
44
+
const job = new SimpleIntervalJob({ seconds: 5 }, task);
45
+
scheduler.addSimpleIntervalJob(job);
49
46
50
47
export const handle = async ({ event, resolve }) => {
51
48
notifyDarkVisitors(event.url, event.request); // no await so it doesnt block
+29
-13
src/lib/lastfm.ts
+29
-13
src/lib/lastfm.ts
···
1
1
import { env } from '$env/dynamic/private';
2
2
import { get, writable } from 'svelte/store';
3
3
4
-
const GET_RECENT_TRACKS_ENDPOINT =
5
-
'https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=yusdacra&api_key=da1911d405b5b37383e200b8f36ee9ec&format=json&limit=1';
4
+
const GET_RECENT_TRACKS_ENDPOINT = 'https://api.listenbrainz.org/1/user/90008/playing-now';
6
5
const LAST_TRACK_FILE = `${env.WEBSITE_DATA_DIR}/last_track.json`;
7
6
8
7
type LastTrack = {
···
15
14
};
16
15
const lastTrack = writable<LastTrack | null>(null);
17
16
18
-
export const lastFmReadLast = async () => {
17
+
export const getLastTrack = async () => {
19
18
try {
20
19
const data = await Deno.readTextFile(LAST_TRACK_FILE);
21
20
lastTrack.set(JSON.parse(data));
22
21
} catch (why) {
23
-
console.log('could not read last fm: ', why);
22
+
console.log('could not read last track: ', why);
24
23
lastTrack.set(null);
25
24
}
26
25
};
27
26
28
-
export const lastFmUpdateNowPlaying = async () => {
27
+
const getTrackCoverArt = (track: any) => {
28
+
// parse origin url to see if it matches youtube.com / music.youtube.com and extract video id
29
+
const originUrl = track.additional_info?.origin_url ?? null;
30
+
if (originUrl && (originUrl.includes('youtube.com') || originUrl.includes('music.youtube.com'))) {
31
+
const videoId = new URL(originUrl).searchParams.get('v');
32
+
if (!videoId) return null;
33
+
return `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`;
34
+
}
35
+
return null;
36
+
};
37
+
38
+
export const updateNowPlayingTrack = async () => {
29
39
try {
30
40
const resp = await (await fetch(GET_RECENT_TRACKS_ENDPOINT)).json();
31
-
const track = resp.recenttracks.track[0] ?? null;
32
-
if (!((track['@attr'] ?? {}).nowplaying ?? null)) {
33
-
throw 'no nowplaying track found';
41
+
const track = resp.payload.listens[0]?.track_metadata;
42
+
if (!track) {
43
+
lastTrack.update((t) => {
44
+
if (t !== null) {
45
+
t.playing = false;
46
+
}
47
+
return t;
48
+
});
49
+
return;
34
50
}
35
51
const data = {
36
-
name: track.name,
37
-
artist: track.artist['#text'],
38
-
image: track.image[2]['#text'] ?? null,
39
-
link: track.url,
52
+
name: track.track_name,
53
+
artist: track.artist_name,
54
+
image: getTrackCoverArt(track),
55
+
link: track.additional_info?.origin_url ?? null,
40
56
when: Date.now(),
41
57
playing: true
42
58
};
···
53
69
}
54
70
};
55
71
56
-
export const getNowPlaying = () => {
72
+
export const getNowPlayingTrack = () => {
57
73
return get(lastTrack);
58
74
};
+7
-1
src/lib/steam.ts
+7
-1
src/lib/steam.ts
···
37
37
try {
38
38
const profile = (await (await fetch(GET_PLAYER_SUMMARY_ENDPOINT)).json()).response.players[0];
39
39
if (!profile.gameid) {
40
-
throw 'no game is being played';
40
+
lastGame.update((t) => {
41
+
if (t !== null) {
42
+
t.playing = false;
43
+
}
44
+
return t;
45
+
});
46
+
return;
41
47
}
42
48
const icons = await griddbClient.getIconsBySteamAppId(profile.gameid, ['official', 'custom']);
43
49
//console.log(icons)
+1
-1
src/lib/visits.ts
+1
-1
src/lib/visits.ts
···
131
131
console.log('failed sending dark visitors analytics:', why);
132
132
return null;
133
133
})
134
-
.then(async (resp) => {
134
+
.then((resp) => {
135
135
if (resp !== null) {
136
136
const host = `(${request.headers.get('host')}|${request.headers.get('x-real-ip')}|${request.headers.get('user-agent')})`;
137
137
console.log(`sent visitor analytic to dark visitors: ${resp.statusText}; ${host}`);
+2
-2
src/routes/(site)/+page.server.ts
+2
-2
src/routes/(site)/+page.server.ts
···
1
1
import { getLastPosts } from '$lib/bluesky.js';
2
-
import { getNowPlaying } from '$lib/lastfm';
2
+
import { getNowPlayingTrack } from '$lib/lastfm';
3
3
import { getLastGame } from '$lib/steam';
4
4
import { noteFromBskyPost } from '$components/note.svelte';
5
5
import { pushNotification } from '$lib/pushnotif';
···
8
8
import { useToken as checkApiToken } from '$lib/apiToken.js';
9
9
10
10
export const load = async () => {
11
-
const lastTrack = getNowPlaying();
11
+
const lastTrack = getNowPlayingTrack();
12
12
const lastGame = getLastGame();
13
13
const lastPosts = getLastPosts();
14
14
const lastNote = lastPosts.length > 0 ? noteFromBskyPost(lastPosts[0]) : null;
+2
-2
src/routes/(site)/+page.svelte
+2
-2
src/routes/(site)/+page.svelte
···
162
162
<!-- svelte-ignore a11y_missing_attribute -->
163
163
{#if data.lastTrack.image}
164
164
<img
165
-
class="border-4 w-[4.5rem] h-[4.5rem]"
165
+
class="border-4 w-[4.5rem] h-[4.5rem] object-cover"
166
166
style="border-style: none double none none;"
167
167
src={data.lastTrack.image}
168
168
/>
···
182
182
>
183
183
<a
184
184
title={data.lastTrack.name}
185
-
href="https://www.last.fm/user/yusdacra"
185
+
href={data.lastTrack.link ?? 'https://listenbrainz.org/user/90008/'}
186
186
class="hover:underline motion-safe:hover:animate-squiggle">{data.lastTrack.name}</a
187
187
>
188
188
</p>