89 lines
2.5 KiB
TypeScript
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 })
|
|
}
|
|
}
|