Manual channel curation and fuzzy channel searching #1
@@ -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',
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest } from "next/server"
|
||||
import Fuse from "fuse.js"
|
||||
import { debugLog } from "@/utils/debugLog";
|
||||
|
||||
type ChannelInfo = {
|
||||
id: string,
|
||||
@@ -18,41 +19,98 @@ type ChannelInfo = {
|
||||
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) {
|
||||
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const name = searchParams.get('name')
|
||||
const country = searchParams.get('country')
|
||||
|
||||
if (!name) {
|
||||
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 channelInfo = channel ?? { id: manualId, name }
|
||||
|
||||
return new Response(JSON.stringify(channelInfo), {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Memoize fuses
|
||||
if (!memoFuses[country ?? ""]) {
|
||||
debugLog(`Creating memoFuse for ${country ?? "null"}`)
|
||||
|
||||
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: [
|
||||
"name",
|
||||
"alt_names",
|
||||
"id"
|
||||
],
|
||||
includeScore: true,
|
||||
distance: 20
|
||||
})
|
||||
fuse = memoFuses[country ?? ""]
|
||||
} else {
|
||||
fuse = memoFuses[country ?? ""]
|
||||
}
|
||||
|
||||
const limitedChannelData = channelData.filter(c => ["US", "CA", "UK"].includes(c.country))
|
||||
|
||||
// const expandedChannelData = channelData.map(c => ({
|
||||
// ...c,
|
||||
// all_names: [c.name, ...c.alt_names]
|
||||
// }))
|
||||
const found: { item: ChannelInfo }[] = fuse.search(name, { limit: 1 })
|
||||
const foundChannel: ChannelInfo | null = found[0].item ?? null
|
||||
|
||||
// const filteredChannelData = expandedChannelData.filter(c => c.all_names.includes(name))
|
||||
|
||||
const fuse = new Fuse(limitedChannelData, {
|
||||
keys: [
|
||||
"name",
|
||||
"alt_names",
|
||||
"id"
|
||||
],
|
||||
includeScore: true,
|
||||
distance: 20
|
||||
})
|
||||
|
||||
const found = fuse.search(name, { limit: 10 })
|
||||
|
||||
return new Response(JSON.stringify(found), {
|
||||
return new Response(foundChannel ? JSON.stringify(foundChannel) : null, {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user