ai generated solutions to our ai generated problems
This commit is contained in:
+2
-214
@@ -46,9 +46,6 @@ const UPTIME_HISTORY_LIMIT = Number(process.env.UPTIME_HISTORY_LIMIT || 336)
|
||||
const ANALYTICS_DATABASE_FILE = process.env.ANALYTICS_DATABASE_FILE || 'viewers.sqlite'
|
||||
const ANALYTICS_RETENTION_DAYS = Number(process.env.ANALYTICS_RETENTION_DAYS || 30)
|
||||
const ANALYTICS_ACTIVE_WINDOW_SECONDS = Number(process.env.ANALYTICS_ACTIVE_WINDOW_SECONDS || 75)
|
||||
const LASTFM_API_KEY = process.env.LASTFM_API_KEY || ''
|
||||
const LASTFM_USERNAME = process.env.LASTFM_USERNAME || ''
|
||||
const LASTFM_HISTORY_FILE = process.env.LASTFM_HISTORY_FILE || 'lastfm-song-of-day.json'
|
||||
const API_CACHE_TTL_MS = Number(process.env.API_CACHE_TTL_MS || 15000)
|
||||
const API_RATE_LIMIT_WINDOW_MS = Number(process.env.API_RATE_LIMIT_WINDOW_MS || 60000)
|
||||
const API_RATE_LIMIT_MAX = Number(process.env.API_RATE_LIMIT_MAX || 120)
|
||||
@@ -129,7 +126,7 @@ const CSP_DIRECTIVES = [
|
||||
"style-src 'self'",
|
||||
"style-src-elem 'self'",
|
||||
"style-src-attr 'unsafe-inline'",
|
||||
"img-src 'self' data: blob: https://*.basemaps.cartocdn.com https://basemaps.cartocdn.com https://lastfm.freetls.fastly.net https://*.lastfm.freetls.fastly.net",
|
||||
"img-src 'self' data: blob: https://*.basemaps.cartocdn.com https://basemaps.cartocdn.com",
|
||||
"media-src https://*.dzcdn.net https://*.deezer.com",
|
||||
"font-src 'self' data:",
|
||||
"connect-src 'self' https://challenges.cloudflare.com",
|
||||
@@ -182,199 +179,6 @@ let uptimeDb = null
|
||||
let analyticsDb = null
|
||||
let latestUptimeSnapshot = null
|
||||
|
||||
function songHistoryPath() {
|
||||
const fileName = path.basename(LASTFM_HISTORY_FILE || 'lastfm-song-of-day.json')
|
||||
return path.join(uptimeStoragePath(), fileName)
|
||||
}
|
||||
|
||||
function readSongHistory() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(songHistoryPath(), 'utf8'))
|
||||
} catch {
|
||||
return { picks: [] }
|
||||
}
|
||||
}
|
||||
|
||||
function writeSongHistory(history) {
|
||||
const storageDir = uptimeStoragePath()
|
||||
fs.mkdirSync(storageDir, { recursive: true })
|
||||
fs.writeFileSync(songHistoryPath(), JSON.stringify(history, null, 2))
|
||||
}
|
||||
|
||||
function todayKey() {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function lastfmImage(track) {
|
||||
const images = Array.isArray(track.image) ? track.image : []
|
||||
const image = [...images].reverse().find((item) => item?.['#text'])
|
||||
const url = image?.['#text'] || ''
|
||||
if (!url) return ''
|
||||
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
if (parsed.protocol !== 'https:') return ''
|
||||
if (parsed.hostname !== 'lastfm.freetls.fastly.net' && !parsed.hostname.endsWith('.lastfm.freetls.fastly.net')) return ''
|
||||
return parsed.toString()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLastfmTrack(track) {
|
||||
const artist = track?.artist?.['#text'] || track?.artist?.name || ''
|
||||
const name = track?.name || ''
|
||||
if (!artist || !name) return null
|
||||
|
||||
return {
|
||||
id: `${artist.toLowerCase()}::${name.toLowerCase()}`,
|
||||
artist,
|
||||
name,
|
||||
album: track?.album?.['#text'] || '',
|
||||
url: track?.url || '',
|
||||
image: lastfmImage(track),
|
||||
played_at: track?.date?.uts ? Number(track.date.uts) : null,
|
||||
}
|
||||
}
|
||||
|
||||
function randomItem(items) {
|
||||
if (!items.length) return null
|
||||
const index = crypto.randomInt(0, items.length)
|
||||
return items[index]
|
||||
}
|
||||
|
||||
function normalizeComparable(value) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/&/g, ' and ')
|
||||
.replace(/[^a-z0-9]+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function previewUrl(value) {
|
||||
if (!value) return ''
|
||||
|
||||
try {
|
||||
const parsed = new URL(value)
|
||||
if (parsed.protocol === 'http:') parsed.protocol = 'https:'
|
||||
if (parsed.protocol !== 'https:') return ''
|
||||
if (!parsed.hostname.endsWith('.dzcdn.net') && !parsed.hostname.endsWith('.deezer.com')) return ''
|
||||
if (!parsed.pathname.endsWith('.mp3')) return ''
|
||||
return parsed.toString()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
async function findDeezerPreview(track) {
|
||||
if (!track?.artist || !track?.name) return null
|
||||
|
||||
const url = new URL('https://api.deezer.com/search/track')
|
||||
url.searchParams.set('q', `${track.artist} ${track.name}`)
|
||||
url.searchParams.set('limit', '5')
|
||||
|
||||
const result = await requestJson(url.toString(), 8000)
|
||||
const candidates = Array.isArray(result.body?.data) ? result.body.data : []
|
||||
const targetArtist = normalizeComparable(track.artist)
|
||||
const targetTitle = normalizeComparable(track.name)
|
||||
|
||||
const matched = candidates.find((candidate) => {
|
||||
const candidateArtist = normalizeComparable(candidate?.artist?.name)
|
||||
const candidateTitle = normalizeComparable(candidate?.title_short || candidate?.title)
|
||||
if (!candidateArtist || !candidateTitle) return false
|
||||
return (
|
||||
candidateTitle === targetTitle &&
|
||||
(candidateArtist === targetArtist ||
|
||||
candidateArtist.includes(targetArtist) ||
|
||||
targetArtist.includes(candidateArtist))
|
||||
)
|
||||
}) || candidates[0]
|
||||
|
||||
const preview = previewUrl(matched?.preview)
|
||||
if (!preview) return null
|
||||
|
||||
return {
|
||||
preview_url: preview,
|
||||
preview_provider: 'Deezer',
|
||||
preview_track_url: matched?.link || '',
|
||||
}
|
||||
}
|
||||
|
||||
async function songOfTheDay() {
|
||||
if (!LASTFM_API_KEY || !LASTFM_USERNAME) {
|
||||
return {
|
||||
configured: false,
|
||||
error: 'Last.fm is not configured',
|
||||
}
|
||||
}
|
||||
|
||||
const date = todayKey()
|
||||
const history = readSongHistory()
|
||||
const existing = (history.picks || []).find((pick) => pick.date === date)
|
||||
if (existing?.track) {
|
||||
if (!existing.track.preview_url) {
|
||||
try {
|
||||
const preview = await findDeezerPreview(existing.track)
|
||||
if (preview) {
|
||||
existing.track = { ...existing.track, ...preview }
|
||||
writeSongHistory(history)
|
||||
}
|
||||
} catch {
|
||||
// The Last.fm card still works without a preview.
|
||||
}
|
||||
}
|
||||
return { configured: true, date, track: existing.track }
|
||||
}
|
||||
|
||||
const to = Math.floor(Date.now() / 1000)
|
||||
const from = to - 7 * 24 * 60 * 60
|
||||
const url = new URL('https://ws.audioscrobbler.com/2.0/')
|
||||
url.searchParams.set('method', 'user.getrecenttracks')
|
||||
url.searchParams.set('user', LASTFM_USERNAME)
|
||||
url.searchParams.set('api_key', LASTFM_API_KEY)
|
||||
url.searchParams.set('format', 'json')
|
||||
url.searchParams.set('from', String(from))
|
||||
url.searchParams.set('to', String(to))
|
||||
url.searchParams.set('limit', '200')
|
||||
|
||||
const result = await requestJson(url.toString(), 10000)
|
||||
const tracks = (result.body?.recenttracks?.track || [])
|
||||
.map(normalizeLastfmTrack)
|
||||
.filter(Boolean)
|
||||
|
||||
const uniqueTracks = Array.from(new Map(tracks.map((track) => [track.id, track])).values())
|
||||
if (!uniqueTracks.length) {
|
||||
return {
|
||||
configured: true,
|
||||
date,
|
||||
error: 'No Last.fm tracks found from the last week',
|
||||
}
|
||||
}
|
||||
|
||||
const previousIds = new Set((history.picks || []).map((pick) => pick.track?.id).filter(Boolean))
|
||||
const available = uniqueTracks.filter((track) => !previousIds.has(track.id))
|
||||
let track = randomItem(available.length ? available : uniqueTracks)
|
||||
try {
|
||||
const preview = await findDeezerPreview(track)
|
||||
if (preview) track = { ...track, ...preview }
|
||||
} catch {
|
||||
// Preview playback is opportunistic; metadata should still render.
|
||||
}
|
||||
const nextHistory = {
|
||||
picks: [
|
||||
...(history.picks || []).filter((pick) => pick.date !== date),
|
||||
{ date, track },
|
||||
]
|
||||
.sort((a, b) => String(a.date).localeCompare(String(b.date)))
|
||||
.slice(-366),
|
||||
}
|
||||
|
||||
writeSongHistory(nextHistory)
|
||||
return { configured: true, date, track }
|
||||
}
|
||||
|
||||
function sendJson(res, status, body, headers = {}) {
|
||||
send(res, status, JSON.stringify(body), { ...jsonHeaders, ...headers })
|
||||
}
|
||||
@@ -2085,6 +1889,7 @@ function htmlWithSeo(req, data) {
|
||||
.replaceAll('__SEO_ROBOTS__', escapeHtml(seo.robots))
|
||||
.replaceAll('__SEO_CANONICAL__', escapeHtml(canonicalUrl))
|
||||
.replaceAll('__SEO_JSON_LD__', routeStructuredData(origin, seo, canonicalUrl).replace(/</g, '\\u003c'))
|
||||
.replaceAll('__TURNSTILE_SESSION__', isTurnstileSessionVerified(req) ? 'verified' : 'required')
|
||||
}
|
||||
|
||||
function sendHtml(req, res, data, status = 200) {
|
||||
@@ -2256,23 +2061,6 @@ const server = http.createServer((req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && req.url === '/api/song-of-day') {
|
||||
if (!isSameOriginRequest(req)) {
|
||||
sendJson(res, 403, { error: 'Song of the day is restricted to this site' })
|
||||
return
|
||||
}
|
||||
|
||||
if (isRateLimited(req)) {
|
||||
sendJson(res, 429, { error: 'Too many requests' }, { 'retry-after': String(Math.ceil(API_RATE_LIMIT_WINDOW_MS / 1000)) })
|
||||
return
|
||||
}
|
||||
|
||||
songOfTheDay()
|
||||
.then((data) => sendJson(res, data.error ? 503 : 200, data))
|
||||
.catch((error) => sendJson(res, 502, { configured: Boolean(LASTFM_API_KEY && LASTFM_USERNAME), error: 'Last.fm request failed', detail: error.message }))
|
||||
return
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url === '/api/viewers/event') {
|
||||
if (!isSameOriginRequest(req)) {
|
||||
sendJson(res, 403, { error: 'Analytics events are restricted to this site' })
|
||||
|
||||
Reference in New Issue
Block a user