jefflix-website/app/api/radio/channels/[placeId]/route.ts

89 lines
2.5 KiB
TypeScript

import { NextResponse } from 'next/server'
const RADIO_GARDEN_API = 'https://radio.garden/api'
const HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json',
'Referer': 'https://radio.garden/',
}
interface RGChannelItem {
page: {
url: string
title: string
place: { id: string; title: string }
country: { id: string; title: string }
website?: string
}
}
interface RGChannelsResponse {
data: {
title: string
content: Array<{ items: RGChannelItem[] }>
}
}
export interface SlimChannel {
id: string
title: string
placeTitle: string
country: string
website?: string
}
// Simple LRU-ish cache: map with max 500 entries
const channelCache = new Map<string, { data: SlimChannel[]; fetchedAt: number }>()
const CACHE_TTL = 60 * 60 * 1000 // 1 hour
const MAX_CACHE = 500
function extractChannelId(url: string): string {
// url format: "/listen/kcsm/HQQcSxCf"
const parts = url.split('/')
return parts[parts.length - 1]
}
export async function GET(
_request: Request,
{ params }: { params: Promise<{ placeId: string }> }
) {
const { placeId } = await params
try {
const cached = channelCache.get(placeId)
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) {
return NextResponse.json(cached.data)
}
const res = await fetch(`${RADIO_GARDEN_API}/ara/content/page/${placeId}/channels`, {
headers: HEADERS,
signal: AbortSignal.timeout(10000),
})
if (!res.ok) throw new Error(`Radio Garden returned ${res.status}`)
const json: RGChannelsResponse = await res.json()
const channels: SlimChannel[] = (json.data.content?.[0]?.items || []).map((item) => ({
id: extractChannelId(item.page.url),
title: item.page.title,
placeTitle: item.page.place.title,
country: item.page.country.title,
website: item.page.website,
}))
// Evict oldest if over limit
if (channelCache.size >= MAX_CACHE) {
const oldest = channelCache.keys().next().value
if (oldest) channelCache.delete(oldest)
}
channelCache.set(placeId, { data: channels, fetchedAt: Date.now() })
return NextResponse.json(channels)
} catch (error) {
console.error(`Failed to fetch channels for ${placeId}:`, error)
const cached = channelCache.get(placeId)
if (cached) return NextResponse.json(cached.data)
return NextResponse.json({ error: 'Failed to load channels' }, { status: 502 })
}
}