Memoized fuses

This commit is contained in:
2025-08-25 22:36:28 -04:00
parent 3cb503d927
commit 99c1b861c7
2 changed files with 154 additions and 22 deletions
+74
View File
@@ -0,0 +1,74 @@
import { debugLog } from "@/utils/debugLog"
import { NextRequest } from "next/server"
export async function GET(request: NextRequest) {
const response = await fetch(`https://sdlive-1-internal.d-ho.me/playlist.m3u8`)
if (!response.ok) {
return new Response('Failed to fetch playlist', { status: 500 })
}
const playlist = await response.text()
const lines = playlist.split("\n")
debugLog(lines)
const channels: string[] = []
for (let i = 1; i + 1 < lines.length; i += 2) {
const info = lines[i]
const streamURL = lines[i + 1]
debugLog(`Attempting to parse: info: ${info}. streamURL: ${streamURL}`)
// Basic validation to hope that assumptions are valid
if (!info.startsWith("#EXTINF:-1")) {
console.error(`iptv/sdlive-playlist.m3u8: One of the info lines did not start with #EXTINF:. Instead got: ${info}`)
return new Response("Internal Server Error", { status: 500 })
} else if (!streamURL.match(/\/stream\/[0-9]+\.m3u8$/)) {
console.error(`iptv/sdlive-playlist.m3u8: One of the streamURLs did not match expected ending. Instead got: ${streamURL}`)
return new Response("Internal Server Error", { status: 500 })
}
const channelName = info.split(",").at(-1)
if (channelName) channels.push(channelName)
}
const oneoffs = ["BBC America (BBCA)", "CBC CA", "CBS Sports Network (CBSSN)", "CTV 2 Canada", "CTV Canada", "Citytv ", "Discovery Channel", "Discovery Family", "Discovery Life Channel", "Disney", "E! Entertainment Television", "FX Movie Channel", "FYI", "Fox News", "Freeform", "Game Show Network", "Global CA", "HGTV", "MSNBC ", "NBC Sports", "NICK", "National Geographic", "Oprah Winfrey Network (OWN)", "Oxygen True Crime", "Paramount Network", "RDS CA", "RDS 2 CA", "TLC ", "The Food Network", "The Hallmark Channel", "The Weather Channel", "VICE TV", "WWE Network"]
const filteredChannels = channels.filter(name => {
if (name.includes("USA")) return true
else if (name.includes("UK") && name.match(/ITV|BBC/)) return true
else if (name.includes("TSN") && name.match(/[1-5]/g)) return true
else if (name.includes("Sportsnet") && name.match(/360|East|One|Ontario|West|World/g)) return true
else if (oneoffs.includes(name)) return true
else return false
})
const url = new URL(request.url)
const baseUrl = `${url.protocol}//${url.host}`
const nameToIDMap: Record<string, string> = {}
await Promise.all(filteredChannels.map(async channelName => {
const country = channelName.endsWith("USA")
? "US" :
channelName.endsWith("CA") || channelName.endsWith("Canada")
? "CA" :
channelName.endsWith("UK")
? "UK" :
null
const response = await fetch(`${baseUrl}/iptv/channel?name=${encodeURIComponent(channelName)}${country ? `&country=${country}` : ""}`)
const channelInfo = await response.json()
const foundId: string = channelInfo[0].item.id
nameToIDMap[channelName] = foundId
}))
return new Response(JSON.stringify(nameToIDMap), {
headers: {
'Content-Type': 'application/json',
}
})
}
+69 -11
View File
@@ -1,5 +1,6 @@
import { NextRequest } from "next/server" import { NextRequest } from "next/server"
import Fuse from "fuse.js" import Fuse from "fuse.js"
import { debugLog } from "@/utils/debugLog";
type ChannelInfo = { type ChannelInfo = {
id: string, id: string,
@@ -18,29 +19,80 @@ type ChannelInfo = {
website: string | null website: string | null
} }
const manualNameToIdMappings: Record<string, string> = {
// "5 USA": "5USA.uk",
// "Oxygen True Crime": "TrueCrime.uk",
// "CBC CA": "CBAFTDT.ca",
// "CMT USA": "CMTMusic.us",
// "Sportsnet West": "SportsnetWorld.ca",
// "Sportsnet East": "Sportsnet.ca",
// "Fox Sports 1 USA": "FoxSports2LatinAmerica.us",
// "BET USA": "IBTVUSA.us",
// "FOXNY USA": "FoxBusinessNetwork.us",
// "CBSNY USA": "CBNEspanol.us",
// "ABCNY USA": "MBC1USA.us",
// "Nat Geo Wild USA": "NatGeoKidsLatinAmerica.us",
// "CTV 2 Canada": "GTNCanada.ca",
// "AMC USA": "AMCRush.us",
// "ESPN USA": "ESPN3LatinAmerica.us",
// "MTV USA": "IBTVUSA.us",
// "IFC TV USA": "IndTVUSA.us",
// "FOX USA": "FoxBusinessNetwork.us",
// "Sportsnet Ontario": "SportsnetOne.ca",
// "NBCNY USA": "MBC1USA.us",
// "Showtime SHOxBET USA": "ShowtimeShowcase.us",
// "ESPN2 USA": "ESPN3LatinAmerica.us",
// "MY9TV USA": "MMCTVUSA.us",
// "MAVTV USA": "MMCTVUSA.us",
// "CW USA": "WUSADT1.us",
// "Discovery Life Channel": "DiscoveryChannel.ca",
// "The Food Network": "TheWordNetwork.us",
// "WETV USA": "IBTVUSA.us",
// "ION USA": "HmongUSATV.us",
// "CTV Canada": "GTNCanada.ca",
}
let memoChannelData: ChannelInfo[] | null = null;
let memoFuses: Record<string, Fuse<ChannelInfo>> = {}
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams const searchParams = request.nextUrl.searchParams
const name = searchParams.get('name') const name = searchParams.get('name')
const country = searchParams.get('country')
if (!name) { if (!name) {
return new Response("Must be supplied with a name to search", { status: 400 }) return new Response("Must be supplied with a name to search", { status: 400 })
} }
const channelResp = await fetch("https://iptv-org.github.io/api/channels.json") let fuse: Fuse<ChannelInfo> | null = null;
const channelData = memoChannelData || (await (await fetch("https://iptv-org.github.io/api/channels.json")).json() as ChannelInfo[])
memoChannelData = channelData
const channelData: ChannelInfo[] = await channelResp.json() // Skip logic for manual mappings
if (manualNameToIdMappings[name]) {
const manualId = manualNameToIdMappings[name]
const channel: ChannelInfo | undefined = channelData.find(data => data.id === manualId)
const limitedChannelData = channelData.filter(c => ["US", "CA", "UK"].includes(c.country)) const channelInfo = channel ?? { id: manualId, name }
// const expandedChannelData = channelData.map(c => ({ return new Response(JSON.stringify(channelInfo), {
// ...c, headers: {
// all_names: [c.name, ...c.alt_names] "Content-Type": "application/json"
// })) }
})
}
// const filteredChannelData = expandedChannelData.filter(c => c.all_names.includes(name)) // Memoize fuses
if (!memoFuses[country ?? ""]) {
debugLog(`Creating memoFuse for ${country ?? "null"}`)
const fuse = new Fuse(limitedChannelData, { const limitedChannelData = country
? channelData.filter(c => c.country === country.toUpperCase())
: channelData.filter(c => ["US", "CA", "UK"].includes(c.country))
memoFuses[country ?? ""] = new Fuse(limitedChannelData, {
keys: [ keys: [
"name", "name",
"alt_names", "alt_names",
@@ -49,10 +101,16 @@ export async function GET(request: NextRequest) {
includeScore: true, includeScore: true,
distance: 20 distance: 20
}) })
fuse = memoFuses[country ?? ""]
} else {
fuse = memoFuses[country ?? ""]
}
const found = fuse.search(name, { limit: 10 })
return new Response(JSON.stringify(found), { const found: { item: ChannelInfo }[] = fuse.search(name, { limit: 1 })
const foundChannel: ChannelInfo | null = found[0].item ?? null
return new Response(foundChannel ? JSON.stringify(foundChannel) : null, {
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
} }