tangled
alpha
login
or
join now
ptr.pet
/
endpoint
0
fork
atom
data endpoint for entity 90008 (aka. a website)
0
fork
atom
overview
issues
pulls
pipelines
cache coverartarchive requests
ptr.pet
3 months ago
6e5130b2
38bc6dce
verified
This commit was signed with the committer's
known signature
.
ptr.pet
SSH Key Fingerprint:
SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw=
0/0
Waiting for spindle ...
+123
-54
4 changed files
expand all
collapse all
unified
split
eunomia
src
lib
lastfm.ts
robots.ts
routes
(site)
+page.svelte
cover_art
[mbid]
+server.ts
+78
-28
eunomia/src/lib/lastfm.ts
···
4
4
const DID = 'did:plc:dfl62fgb7wtjj3fcbb72naae';
5
5
const PDS = 'https://zwsp.xyz';
6
6
const LAST_TRACK_FILE = `${env.WEBSITE_DATA_DIR}/last_track.json`;
7
7
+
const COVER_ART_CACHE_DIR = `${env.WEBSITE_DATA_DIR}/cover_art_cache`;
7
8
8
9
type LastTrack = {
9
10
name: string;
10
11
artist: string;
11
12
album: string;
12
12
-
images: {
13
13
-
mb: string | null;
14
14
-
yt: string | null;
15
15
-
};
13
13
+
image: string | null; // Single image URL
16
14
link: string | null;
17
15
when: number;
18
16
status: 'playing' | 'played';
19
17
};
20
18
const lastTrack = writable<LastTrack | null>(null);
21
19
22
22
-
export const getLastTrack = async () => {
20
20
+
// Ensure cache directory exists
21
21
+
const ensureCacheDir = async () => {
23
22
try {
24
24
-
const data = await Deno.readTextFile(LAST_TRACK_FILE);
25
25
-
lastTrack.set(JSON.parse(data));
26
26
-
} catch (why) {
27
27
-
console.log('could not read last track: ', why);
28
28
-
lastTrack.set(null);
23
23
+
await Deno.mkdir(COVER_ART_CACHE_DIR, { recursive: true });
24
24
+
} catch (err) {
25
25
+
// Directory might already exist, ignore error
29
26
}
30
27
};
31
28
32
32
-
const getTrackCoverArt = (
33
33
-
releaseMbId: string | null | undefined,
34
34
-
originUrl: string | null | undefined
35
35
-
) => {
36
36
-
let mb: string | null = null;
37
37
-
let yt: string | null = null;
29
29
+
// Fetch and cache MusicBrainz cover art
30
30
+
const fetchAndCacheCoverArt = async (releaseMbId: string): Promise<string | null> => {
31
31
+
const cacheFile = `${COVER_ART_CACHE_DIR}/${releaseMbId}.jpg`;
38
32
39
39
-
if (releaseMbId) mb = `https://coverartarchive.org/release/${releaseMbId}/front-250`;
33
33
+
// Check if already cached
34
34
+
try {
35
35
+
await Deno.stat(cacheFile);
36
36
+
return `/cover_art/${releaseMbId}.jpg`;
37
37
+
} catch {
38
38
+
// Not cached, try to fetch
39
39
+
}
40
40
41
41
try {
42
42
-
if (originUrl) {
43
43
-
let videoId: string | null = null;
44
44
-
if (originUrl.includes('youtube.com') || originUrl.includes('music.youtube.com')) {
45
45
-
videoId = new URL(originUrl).searchParams.get('v');
46
46
-
} else if (originUrl.includes('youtu.be')) {
47
47
-
videoId = originUrl.split('youtu.be/')[1]?.split('?')[0];
48
48
-
}
49
49
-
if (videoId) yt = `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`;
42
42
+
const mbUrl = `https://coverartarchive.org/release/${releaseMbId}/front-250`;
43
43
+
const response = await fetch(mbUrl);
44
44
+
45
45
+
if (!response.ok) {
46
46
+
return null;
47
47
+
}
48
48
+
49
49
+
const imageData = await response.arrayBuffer();
50
50
+
await Deno.writeFile(cacheFile, new Uint8Array(imageData));
51
51
+
52
52
+
return `/cover_art/${releaseMbId}.jpg`;
53
53
+
} catch (err) {
54
54
+
console.log(`Failed to fetch MusicBrainz cover art for ${releaseMbId}:`, err);
55
55
+
return null;
56
56
+
}
57
57
+
};
58
58
+
59
59
+
// Get YouTube thumbnail URL
60
60
+
const getYouTubeThumbnail = (originUrl: string | null | undefined): string | null => {
61
61
+
if (!originUrl) return null;
62
62
+
63
63
+
try {
64
64
+
let videoId: string | null = null;
65
65
+
if (originUrl.includes('youtube.com') || originUrl.includes('music.youtube.com')) {
66
66
+
videoId = new URL(originUrl).searchParams.get('v');
67
67
+
} else if (originUrl.includes('youtu.be')) {
68
68
+
videoId = originUrl.split('youtu.be/')[1]?.split('?')[0];
69
69
+
}
70
70
+
if (videoId) {
71
71
+
return `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`;
50
72
}
51
73
} catch {}
52
74
53
53
-
return { mb, yt };
75
75
+
return null;
76
76
+
};
77
77
+
78
78
+
// Get cover art with caching
79
79
+
const getCoverArt = async (
80
80
+
releaseMbId: string | null | undefined,
81
81
+
originUrl: string | null | undefined
82
82
+
): Promise<string | null> => {
83
83
+
// Try MusicBrainz first (with caching)
84
84
+
if (releaseMbId) {
85
85
+
const mbImage = await fetchAndCacheCoverArt(releaseMbId);
86
86
+
if (mbImage) return mbImage;
87
87
+
}
88
88
+
89
89
+
// Fall back to YouTube thumbnail
90
90
+
return getYouTubeThumbnail(originUrl);
91
91
+
};
92
92
+
93
93
+
export const getLastTrack = async () => {
94
94
+
try {
95
95
+
const data = await Deno.readTextFile(LAST_TRACK_FILE);
96
96
+
lastTrack.set(JSON.parse(data));
97
97
+
} catch (why) {
98
98
+
console.log('could not read last track: ', why);
99
99
+
lastTrack.set(null);
100
100
+
}
54
101
};
55
102
56
103
const joinArtists = (artists: any[]) => {
57
104
if (!artists || artists.length === 0) return null;
58
58
-
// remove duplicates
59
105
const uniqueArtists = [...new Set(artists.map((a) => a.artistName))];
60
106
return uniqueArtists.join(', ');
61
107
};
62
108
63
109
export const updateNowPlayingTrack = async () => {
110
110
+
await ensureCacheDir();
111
111
+
64
112
try {
65
113
let track: any = null;
66
114
let when: number = Date.now();
···
86
134
87
135
if (!track) return;
88
136
137
137
+
const coverArt = await getCoverArt(track.releaseMbId, track.originUrl);
138
138
+
89
139
const data: LastTrack = {
90
140
name: track.trackName,
91
141
artist: joinArtists(track.artists) ?? 'Unknown Artist',
92
142
album: track.releaseName ?? 'Unknown Album',
93
93
-
images: getTrackCoverArt(track.releaseMbId, track.originUrl),
143
143
+
image: coverArt,
94
144
link: track.originUrl ?? null,
95
145
when: when,
96
146
status: status
+15
-10
eunomia/src/lib/robots.ts
···
11
11
const lastFetched = writable<number>(Date.now());
12
12
13
13
const fetchRobotsTxt = async () => {
14
14
-
const robotsTxt = await darkVisitors.generateRobotsTxt([
15
15
-
AgentType.AIAgent,
16
16
-
AgentType.AIAssistant,
17
17
-
AgentType.AIDataScraper,
18
18
-
AgentType.AISearchCrawler,
19
19
-
AgentType.UndocumentedAIAgent,
20
20
-
AgentType.SEOCrawler
21
21
-
]);
22
22
-
lastFetched.set(Date.now());
23
23
-
return robotsTxt;
14
14
+
try {
15
15
+
const robotsTxt = await darkVisitors.generateRobotsTxt([
16
16
+
AgentType.AIAgent,
17
17
+
AgentType.AIAssistant,
18
18
+
AgentType.AIDataScraper,
19
19
+
AgentType.AISearchCrawler,
20
20
+
AgentType.UndocumentedAIAgent,
21
21
+
AgentType.SEOCrawler
22
22
+
]);
23
23
+
lastFetched.set(Date.now());
24
24
+
return robotsTxt;
25
25
+
} catch (error) {
26
26
+
console.error('failed to fetch robots.txt:', error);
27
27
+
return '';
28
28
+
}
24
29
};
25
30
26
31
export const getRobotsTxt = async () => {
+5
-16
eunomia/src/routes/(site)/+page.svelte
···
168
168
</div>
169
169
{/if}
170
170
{#if data.lastTrack}
171
171
-
{@const images = data.lastTrack.images}
172
172
-
{@const initialUrl = images.mb ?? images.yt}
173
171
{@const showAlbum =
174
172
data.lastTrack.album &&
175
173
data.lastTrack.name.toLowerCase() !== data.lastTrack.album.toLowerCase()}
···
178
176
<img
179
177
class="border-4 {showAlbum
180
178
? 'w-[5.75rem] h-[5.75rem]'
181
181
-
: 'w-[4.5rem] h-[4.5rem]'} {initialUrl ? 'object-cover' : 'p-2'}"
182
182
-
style="border-style: none double none none; {initialUrl
179
179
+
: 'w-[4.5rem] h-[4.5rem]'} {data.lastTrack.image
180
180
+
? 'object-cover'
181
181
+
: 'p-2'}"
182
182
+
style="border-style: none double none none; {data.lastTrack.image
183
183
? ''
184
184
: 'image-rendering: pixelated;'}"
185
185
-
src={initialUrl ?? '/icons/cd_audio.webp'}
185
185
+
src={data.lastTrack.image ?? '/icons/cd_audio.webp'}
186
186
title={data.lastTrack.album}
187
187
-
onerror={(e) => {
188
188
-
const img = e.currentTarget as HTMLImageElement;
189
189
-
if (images.mb && img.src === images.mb && images.yt)
190
190
-
img.src = images.yt;
191
191
-
else {
192
192
-
img.src = '/icons/cd_audio.webp';
193
193
-
img.classList.remove('object-cover');
194
194
-
img.classList.add('p-2');
195
195
-
img.style.imageRendering = 'pixelated';
196
196
-
}
197
197
-
}}
198
187
/>
199
188
<div class="flex flex-col max-w-[60ch] p-2 text-ellipsis overflow-hidden">
200
189
<p
+25
eunomia/src/routes/cover_art/[mbid]/+server.ts
···
1
1
+
import { env } from '$env/dynamic/private';
2
2
+
import { error } from '@sveltejs/kit';
3
3
+
4
4
+
export const GET = async ({ params }) => {
5
5
+
const mbid = params.mbid?.replace('.jpg', '');
6
6
+
7
7
+
if (!mbid) {
8
8
+
throw error(404, 'Missing MBID');
9
9
+
}
10
10
+
11
11
+
const cacheDir = `${env.WEBSITE_DATA_DIR}/cover_art_cache`;
12
12
+
const filePath = `${cacheDir}/${mbid}.jpg`;
13
13
+
14
14
+
try {
15
15
+
const file = await Deno.readFile(filePath);
16
16
+
return new Response(file, {
17
17
+
headers: {
18
18
+
'Content-Type': 'image/jpeg',
19
19
+
'Cache-Control': 'public, max-age=31536000, immutable'
20
20
+
}
21
21
+
});
22
22
+
} catch {
23
23
+
throw error(404, 'cover art not found');
24
24
+
}
25
25
+
};